summaryrefslogtreecommitdiffstats
path: root/roles/lib_openshift
diff options
context:
space:
mode:
Diffstat (limited to 'roles/lib_openshift')
-rw-r--r--roles/lib_openshift/library/oc_adm_ca_server_cert.py1624
-rw-r--r--roles/lib_openshift/library/oc_adm_csr.py1649
-rw-r--r--roles/lib_openshift/library/oc_adm_manage_node.py1671
-rw-r--r--roles/lib_openshift/library/oc_adm_policy_group.py2177
-rw-r--r--roles/lib_openshift/library/oc_adm_policy_user.py2171
-rw-r--r--roles/lib_openshift/library/oc_adm_registry.py2746
-rw-r--r--roles/lib_openshift/library/oc_adm_router.py3191
-rw-r--r--roles/lib_openshift/library/oc_atomic_container.py216
-rw-r--r--roles/lib_openshift/library/oc_clusterrole.py1814
-rw-r--r--roles/lib_openshift/library/oc_configmap.py1631
-rw-r--r--roles/lib_openshift/library/oc_edit.py1591
-rw-r--r--roles/lib_openshift/library/oc_env.py1946
-rw-r--r--roles/lib_openshift/library/oc_group.py1610
-rw-r--r--roles/lib_openshift/library/oc_image.py1536
-rw-r--r--roles/lib_openshift/library/oc_label.py1752
-rw-r--r--roles/lib_openshift/library/oc_obj.py1678
-rw-r--r--roles/lib_openshift/library/oc_objectvalidator.py1475
-rw-r--r--roles/lib_openshift/library/oc_process.py1637
-rw-r--r--roles/lib_openshift/library/oc_project.py1732
-rw-r--r--roles/lib_openshift/library/oc_pvc.py1847
-rw-r--r--roles/lib_openshift/library/oc_route.py1869
-rw-r--r--roles/lib_openshift/library/oc_scale.py1918
-rw-r--r--roles/lib_openshift/library/oc_secret.py1818
-rw-r--r--roles/lib_openshift/library/oc_service.py1865
-rw-r--r--roles/lib_openshift/library/oc_serviceaccount.py1729
-rw-r--r--roles/lib_openshift/library/oc_serviceaccount_secret.py1702
-rw-r--r--roles/lib_openshift/library/oc_storageclass.py1684
-rw-r--r--roles/lib_openshift/library/oc_user.py1764
-rw-r--r--roles/lib_openshift/library/oc_version.py1449
-rw-r--r--roles/lib_openshift/library/oc_volume.py2086
-rw-r--r--roles/lib_openshift/meta/main.yml14
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_ca_server_cert.py40
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_csr.py36
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_manage_node.py38
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_policy_group.py34
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_policy_user.py34
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_registry.py47
-rw-r--r--roles/lib_openshift/src/ansible/oc_adm_router.py69
-rw-r--r--roles/lib_openshift/src/ansible/oc_atomic_container.py150
-rw-r--r--roles/lib_openshift/src/ansible/oc_clusterrole.py29
-rw-r--r--roles/lib_openshift/src/ansible/oc_configmap.py32
-rw-r--r--roles/lib_openshift/src/ansible/oc_edit.py48
-rw-r--r--roles/lib_openshift/src/ansible/oc_env.py33
-rw-r--r--roles/lib_openshift/src/ansible/oc_group.py32
-rw-r--r--roles/lib_openshift/src/ansible/oc_image.py34
-rw-r--r--roles/lib_openshift/src/ansible/oc_label.py32
-rw-r--r--roles/lib_openshift/src/ansible/oc_obj.py37
-rw-r--r--roles/lib_openshift/src/ansible/oc_objectvalidator.py24
-rw-r--r--roles/lib_openshift/src/ansible/oc_process.py32
-rw-r--r--roles/lib_openshift/src/ansible/oc_project.py33
-rw-r--r--roles/lib_openshift/src/ansible/oc_pvc.py35
-rw-r--r--roles/lib_openshift/src/ansible/oc_route.py50
-rw-r--r--roles/lib_openshift/src/ansible/oc_scale.py29
-rw-r--r--roles/lib_openshift/src/ansible/oc_secret.py39
-rw-r--r--roles/lib_openshift/src/ansible/oc_service.py37
-rw-r--r--roles/lib_openshift/src/ansible/oc_serviceaccount.py30
-rw-r--r--roles/lib_openshift/src/ansible/oc_serviceaccount_secret.py29
-rw-r--r--roles/lib_openshift/src/ansible/oc_storageclass.py32
-rw-r--r--roles/lib_openshift/src/ansible/oc_user.py34
-rw-r--r--roles/lib_openshift/src/ansible/oc_version.py26
-rw-r--r--roles/lib_openshift/src/ansible/oc_volume.py41
-rw-r--r--roles/lib_openshift/src/class/oc_adm_ca_server_cert.py146
-rw-r--r--roles/lib_openshift/src/class/oc_adm_csr.py197
-rw-r--r--roles/lib_openshift/src/class/oc_adm_manage_node.py209
-rw-r--r--roles/lib_openshift/src/class/oc_adm_policy_group.py222
-rw-r--r--roles/lib_openshift/src/class/oc_adm_policy_user.py216
-rw-r--r--roles/lib_openshift/src/class/oc_adm_registry.py430
-rw-r--r--roles/lib_openshift/src/class/oc_adm_router.py500
-rw-r--r--roles/lib_openshift/src/class/oc_clusterrole.py167
-rw-r--r--roles/lib_openshift/src/class/oc_configmap.py191
-rw-r--r--roles/lib_openshift/src/class/oc_edit.py91
-rw-r--r--roles/lib_openshift/src/class/oc_env.py142
-rw-r--r--roles/lib_openshift/src/class/oc_group.py148
-rw-r--r--roles/lib_openshift/src/class/oc_image.py91
-rw-r--r--roles/lib_openshift/src/class/oc_label.py292
-rw-r--r--roles/lib_openshift/src/class/oc_obj.py210
-rw-r--r--roles/lib_openshift/src/class/oc_objectvalidator.py88
-rw-r--r--roles/lib_openshift/src/class/oc_process.py185
-rw-r--r--roles/lib_openshift/src/class/oc_project.py195
-rw-r--r--roles/lib_openshift/src/class/oc_pvc.py169
-rw-r--r--roles/lib_openshift/src/class/oc_route.py208
-rw-r--r--roles/lib_openshift/src/class/oc_scale.py108
-rw-r--r--roles/lib_openshift/src/class/oc_secret.py209
-rw-r--r--roles/lib_openshift/src/class/oc_service.py175
-rw-r--r--roles/lib_openshift/src/class/oc_serviceaccount.py164
-rw-r--r--roles/lib_openshift/src/class/oc_serviceaccount_secret.py138
-rw-r--r--roles/lib_openshift/src/class/oc_storageclass.py155
-rw-r--r--roles/lib_openshift/src/class/oc_user.py227
-rw-r--r--roles/lib_openshift/src/class/oc_version.py47
-rw-r--r--roles/lib_openshift/src/class/oc_volume.py195
-rw-r--r--roles/lib_openshift/src/doc/atomic_container36
-rw-r--r--roles/lib_openshift/src/doc/ca_server_cert102
-rw-r--r--roles/lib_openshift/src/doc/clusterrole66
-rw-r--r--roles/lib_openshift/src/doc/configmap72
-rw-r--r--roles/lib_openshift/src/doc/csr80
-rw-r--r--roles/lib_openshift/src/doc/edit116
-rw-r--r--roles/lib_openshift/src/doc/env83
-rw-r--r--roles/lib_openshift/src/doc/generated10
-rw-r--r--roles/lib_openshift/src/doc/group56
-rw-r--r--roles/lib_openshift/src/doc/image75
-rw-r--r--roles/lib_openshift/src/doc/label92
-rw-r--r--roles/lib_openshift/src/doc/license16
-rw-r--r--roles/lib_openshift/src/doc/manage_node88
-rw-r--r--roles/lib_openshift/src/doc/obj95
-rw-r--r--roles/lib_openshift/src/doc/objectvalidator27
-rw-r--r--roles/lib_openshift/src/doc/policy_group74
-rw-r--r--roles/lib_openshift/src/doc/policy_user74
-rw-r--r--roles/lib_openshift/src/doc/process84
-rw-r--r--roles/lib_openshift/src/doc/project81
-rw-r--r--roles/lib_openshift/src/doc/pvc88
-rw-r--r--roles/lib_openshift/src/doc/registry192
-rw-r--r--roles/lib_openshift/src/doc/route132
-rw-r--r--roles/lib_openshift/src/doc/router217
-rw-r--r--roles/lib_openshift/src/doc/scale70
-rw-r--r--roles/lib_openshift/src/doc/secret128
-rw-r--r--roles/lib_openshift/src/doc/service129
-rw-r--r--roles/lib_openshift/src/doc/serviceaccount68
-rw-r--r--roles/lib_openshift/src/doc/serviceaccount_secret68
-rw-r--r--roles/lib_openshift/src/doc/storageclass86
-rw-r--r--roles/lib_openshift/src/doc/user128
-rw-r--r--roles/lib_openshift/src/doc/version40
-rw-r--r--roles/lib_openshift/src/doc/volume117
-rwxr-xr-xroles/lib_openshift/src/generate.py119
-rw-r--r--roles/lib_openshift/src/lib/base.py608
-rw-r--r--roles/lib_openshift/src/lib/clusterrole.py68
-rw-r--r--roles/lib_openshift/src/lib/deploymentconfig.py350
-rw-r--r--roles/lib_openshift/src/lib/group.py36
-rw-r--r--roles/lib_openshift/src/lib/import.py23
-rw-r--r--roles/lib_openshift/src/lib/project.py85
-rw-r--r--roles/lib_openshift/src/lib/pvc.py217
-rw-r--r--roles/lib_openshift/src/lib/replicationcontroller.py21
-rw-r--r--roles/lib_openshift/src/lib/rolebinding.py289
-rw-r--r--roles/lib_openshift/src/lib/route.py141
-rw-r--r--roles/lib_openshift/src/lib/rule.py144
-rw-r--r--roles/lib_openshift/src/lib/scc.py218
-rw-r--r--roles/lib_openshift/src/lib/secret.py105
-rw-r--r--roles/lib_openshift/src/lib/service.py186
-rw-r--r--roles/lib_openshift/src/lib/serviceaccount.py129
-rw-r--r--roles/lib_openshift/src/lib/storageclass.py73
-rw-r--r--roles/lib_openshift/src/lib/user.py37
-rw-r--r--roles/lib_openshift/src/lib/volume.py43
-rw-r--r--roles/lib_openshift/src/sources.yml328
-rwxr-xr-xroles/lib_openshift/src/test/generate-and-run-tests.sh54
-rw-r--r--roles/lib_openshift/src/test/integration/filter_plugins/test_filters.py28
-rwxr-xr-xroles/lib_openshift/src/test/integration/group.yml229
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_adm_csr.yml28
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_adm_manage_node.yml69
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_clusterrole.yml106
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_configmap.yml95
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_env.yml75
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_label.yml334
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_obj.yml207
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_process.yml83
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_project.yml83
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_pvc.yml28
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_route.yml117
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_scale.yml111
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_secret.yml125
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_service.yml133
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_serviceaccount.yml101
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_serviceaccount_secret.yml79
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_storageclass.yml87
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_user.yml240
-rwxr-xr-xroles/lib_openshift/src/test/integration/oc_version.yml17
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_adm_manage_node.py277
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_adm_registry.py370
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_adm_router.py480
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_clusterrole.py115
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_configmap.py239
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_env.py548
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_group.py253
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_image.py280
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_label.py285
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_objectvalidator.py923
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_process.py572
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_project.py280
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_pvc.py377
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_route.py374
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_scale.py256
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_secret.py192
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_service.py487
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_serviceaccount.py213
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py414
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_storageclass.py93
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_user.py127
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_version.py162
-rwxr-xr-xroles/lib_openshift/src/test/unit/test_oc_volume.py633
-rw-r--r--roles/lib_openshift/tasks/main.yml1
188 files changed, 76693 insertions, 0 deletions
diff --git a/roles/lib_openshift/library/oc_adm_ca_server_cert.py b/roles/lib_openshift/library/oc_adm_ca_server_cert.py
new file mode 100644
index 000000000..05b2763d5
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_ca_server_cert.py
@@ -0,0 +1,1624 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/ca_server_cert -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_ca_server_cert
+short_description: Module to run openshift oc adm ca create-server-cert
+description:
+ - Wrapper around the openshift `oc adm ca create-server-cert` command.
+options:
+ state:
+ description:
+ - Present is the only supported state. The state present means that `oc adm ca` will generate a certificate
+ - and verify if the hostnames and the ClusterIP exists in the certificate.
+ - When create-server-cert is desired then the following parameters are passed.
+ - ['cert', 'key', 'signer_cert', 'signer_key', 'signer_serial']
+ required: false
+ default: present
+ choices:
+ - present
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ cert:
+ description:
+ - The certificate file. Choose a name that indicates what the service is.
+ required: false
+ default: None
+ aliases: []
+ key:
+ description:
+ - The key file. Choose a name that indicates what the service is.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Force updating of the existing cert and key files
+ required: false
+ default: False
+ aliases: []
+ signer_cert:
+ description:
+ - The signer certificate file.
+ required: false
+ default: /etc/origin/master/ca.crt
+ aliases: []
+ signer_key:
+ description:
+ - The signer key file.
+ required: false
+ default: /etc/origin/master/ca.key
+ aliases: []
+ signer_serial:
+ description:
+ - The signer serial file.
+ required: false
+ default: /etc/origin/master/ca.serial.txt
+ aliases: []
+ hostnames:
+ description:
+ - Every hostname or IP that server certs should be valid for
+ required: false
+ default: []
+ aliases: []
+ backup:
+ description:
+ - Whether to backup the cert and key files before writing them.
+ required: false
+ default: True
+ aliases: []
+ expire_days:
+ description
+ - Validity of the certificate in days
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Create a self-signed cert
+ oc_adm_ca_server_cert:
+ signer_cert: /etc/origin/master/ca.crt
+ signer_key: /etc/origin/master/ca.key
+ signer_serial: /etc/origin/master/ca.serial.txt
+ hostnames: "registry.test.openshift.com,127.0.0.1,docker-registry.default.svc.cluster.local"
+ cert: /etc/origin/master/registry.crt
+ key: /etc/origin/master/registry.key
+'''
+
+# -*- -*- -*- End included fragment: doc/ca_server_cert -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_ca_server_cert.py -*- -*- -*-
+
+class CAServerCertConfig(OpenShiftCLIConfig):
+ ''' CAServerCertConfig is a DTO for the oc adm ca command '''
+ def __init__(self, kubeconfig, verbose, ca_options):
+ super(CAServerCertConfig, self).__init__('ca', None, kubeconfig, ca_options)
+ self.kubeconfig = kubeconfig
+ self.verbose = verbose
+ self._ca = ca_options
+
+
+class CAServerCert(OpenShiftCLI):
+ ''' Class to wrap the oc adm ca create-server-cert command line'''
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for oadm ca '''
+ super(CAServerCert, self).__init__(None, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+
+ def get(self):
+ '''get the current cert file
+
+ If a file exists by the same name in the specified location then the cert exists
+ '''
+ cert = self.config.config_options['cert']['value']
+ if cert and os.path.exists(cert):
+ return open(cert).read()
+
+ return None
+
+ def create(self):
+ '''run openshift oc adm ca create-server-cert cmd'''
+
+ # Added this here as a safegaurd for stomping on the
+ # cert and key files if they exist
+ if self.config.config_options['backup']['value']:
+ import time
+ ext = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
+ date_str = "%s_" + "%s" % ext
+
+ if os.path.exists(self.config.config_options['key']['value']):
+ shutil.copy(self.config.config_options['key']['value'],
+ date_str % self.config.config_options['key']['value'])
+ if os.path.exists(self.config.config_options['cert']['value']):
+ shutil.copy(self.config.config_options['cert']['value'],
+ date_str % self.config.config_options['cert']['value'])
+
+ options = self.config.to_option_list()
+
+ cmd = ['ca', 'create-server-cert']
+ cmd.extend(options)
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ def exists(self):
+ ''' check whether the certificate exists and has the clusterIP '''
+
+ cert_path = self.config.config_options['cert']['value']
+ if not os.path.exists(cert_path):
+ return False
+
+ # Would prefer pyopenssl but is not installed.
+ # When we verify it is, switch this code
+ # Here is the code to get the subject and the SAN
+ # openssl x509 -text -noout -certopt \
+ # no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux \
+ # -in /etc/origin/master/registry.crt
+ # Instead of this solution we will use a regex.
+ cert_names = []
+ hostnames = self.config.config_options['hostnames']['value'].split(',')
+ proc = subprocess.Popen(['openssl', 'x509', '-noout', '-text', '-in', cert_path],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ x509output, _ = proc.communicate()
+ if proc.returncode == 0:
+ regex = re.compile(r"^\s*X509v3 Subject Alternative Name:\s*?\n\s*(.*)\s*\n", re.MULTILINE)
+ match = regex.search(x509output.decode()) # E501
+ if not match:
+ return False
+
+ for entry in re.split(r", *", match.group(1)):
+ if entry.startswith('DNS') or entry.startswith('IP Address'):
+ cert_names.append(entry.split(':')[1])
+ # now that we have cert names let's compare
+ cert_set = set(cert_names)
+ hname_set = set(hostnames)
+ if cert_set.issubset(hname_set) and hname_set.issubset(cert_set):
+ return True
+
+ return False
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ # Filter non-strings from hostnames list s.t. the omit filter
+ # may be used to conditionally add a hostname.
+ params['hostnames'] = [host for host in params['hostnames'] if isinstance(host, string_types)]
+
+ config = CAServerCertConfig(params['kubeconfig'],
+ params['debug'],
+ {'cert': {'value': params['cert'], 'include': True},
+ 'hostnames': {'value': ','.join(params['hostnames']), 'include': True},
+ 'overwrite': {'value': True, 'include': True},
+ 'key': {'value': params['key'], 'include': True},
+ 'signer_cert': {'value': params['signer_cert'], 'include': True},
+ 'signer_key': {'value': params['signer_key'], 'include': True},
+ 'signer_serial': {'value': params['signer_serial'], 'include': True},
+ 'expire_days': {'value': params['expire_days'], 'include': True},
+ 'backup': {'value': params['backup'], 'include': False},
+ })
+
+ server_cert = CAServerCert(config)
+
+ state = params['state']
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not server_cert.exists() or params['force']:
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': "CHECK_MODE: Would have created the certificate.",
+ 'state': state}
+
+ api_rval = server_cert.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Exists
+ ########
+ api_rval = server_cert.get()
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'msg': 'Unknown state passed. %s' % state}
+
+
+# -*- -*- -*- End included fragment: class/oc_adm_ca_server_cert.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_ca_server_cert.py -*- -*- -*-
+
+
+# pylint: disable=wrong-import-position
+from ansible.module_utils.six import string_types
+
+def main():
+ '''
+ ansible oc adm module for ca create-server-cert
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str', choices=['present']),
+ debug=dict(default=False, type='bool'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ backup=dict(default=True, type='bool'),
+ force=dict(default=False, type='bool'),
+ # oc adm ca create-server-cert [options]
+ cert=dict(default=None, type='str'),
+ key=dict(default=None, type='str'),
+ signer_cert=dict(default='/etc/origin/master/ca.crt', type='str'),
+ signer_key=dict(default='/etc/origin/master/ca.key', type='str'),
+ signer_serial=dict(default='/etc/origin/master/ca.serial.txt', type='str'),
+ hostnames=dict(default=[], type='list'),
+ expire_days=dict(default=None, type='int'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = CAServerCert.run_ansible(module.params, module.check_mode)
+ if 'failed' in results:
+ return module.fail_json(**results)
+
+ return module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_ca_server_cert.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_csr.py b/roles/lib_openshift/library/oc_adm_csr.py
new file mode 100644
index 000000000..d1dc4caf8
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_csr.py
@@ -0,0 +1,1649 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/csr -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_csr
+short_description: Module to approve or deny openshift certificate signing requests
+description:
+ - Wrapper around the openshift `oc adm certificate approve|deny <csr>` command.
+options:
+ state:
+ description:
+ - approve|deny|list Approve, deny, and list are the only supported states for certificates
+ required: false
+ default: present
+ choices:
+ - present
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ nodes:
+ description:
+ - A list of the names of the nodes in which to accept the certificates
+ required: false
+ default: None
+ aliases: []
+ timeout:
+ description:
+ - This flag allows for a timeout value when approving nodes.
+ required: false
+ default: 30
+ aliases: []
+ timeout:
+ description:
+ - This flag allows for a timeout value when doing node approvals.
+ - A zero value for the timeout will block until the nodes have been accepted
+ required: false
+ default: 30
+ aliases: []
+ approve_all:
+ description:
+ - This flag allows for the module to approve all CSRs that are found.
+ - This facilitates testing.
+ required: false
+ default: False
+ aliases: []
+ service_account:
+ description:
+ - This parameter tells the approval process which service account is being used for the requests
+ required: false
+ default: node-bootstrapper
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Approve certificates for node xyz
+ oc_adm_scr:
+ nodes:
+ - xyz
+ timeout: 300
+
+- name: Approve certificates for node xyz
+ oc_adm_scr:
+ nodes:
+ - xyz
+ timeout: 0
+'''
+
+# -*- -*- -*- End included fragment: doc/csr -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_csr.py -*- -*- -*-
+
+
+class OCcsr(OpenShiftCLI):
+ ''' Class to wrap the oc adm certificate command line'''
+ kind = 'csr'
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ nodes=None,
+ approve_all=False,
+ service_account=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for oc adm certificate '''
+ super(OCcsr, self).__init__(None, kubeconfig, verbose)
+ self.service_account = service_account
+ self.nodes = self.create_nodes(nodes)
+ self._csrs = []
+ self.approve_all = approve_all
+ self.verbose = verbose
+
+ @property
+ def csrs(self):
+ '''property for managing csrs'''
+ # any processing needed??
+ self._csrs = self._get(resource=self.kind)['results'][0]['items']
+ return self._csrs
+
+ def create_nodes(self, nodes):
+ '''create a node object to track csr signing status'''
+ nodes_list = []
+
+ if nodes is None:
+ return nodes_list
+
+ results = self._get(resource='nodes')['results'][0]['items']
+
+ for node in nodes:
+ nodes_list.append(dict(name=node, csrs={}, accepted=False, denied=False))
+
+ for ocnode in results:
+ if node in ocnode['metadata']['name']:
+ nodes_list[-1]['accepted'] = True
+
+ return nodes_list
+
+ def get(self):
+ '''get the current certificate signing requests'''
+ return self.csrs
+
+ @staticmethod
+ def action_needed(csr, action):
+ '''check to see if csr is in desired state'''
+ if csr['status'] == {}:
+ return True
+
+ state = csr['status']['conditions'][0]['type']
+
+ if action == 'approve' and state != 'Approved':
+ return True
+
+ elif action == 'deny' and state != 'Denied':
+ return True
+
+ return False
+
+ def match_node(self, csr):
+ '''match an inc csr to a node in self.nodes'''
+ for node in self.nodes:
+ # we have a match
+ if node['name'] in csr['metadata']['name']:
+ node['csrs'][csr['metadata']['name']] = csr
+
+ # check that the username is the node and type is 'Approved'
+ if node['name'] in csr['spec']['username'] and csr['status']:
+ if csr['status']['conditions'][0]['type'] == 'Approved':
+ node['accepted'] = True
+ # check type is 'Denied' and mark node as such
+ if csr['status'] and csr['status']['conditions'][0]['type'] == 'Denied':
+ node['denied'] = True
+
+ return node
+
+ return None
+
+ def finished(self):
+ '''determine if there are more csrs to sign'''
+ # if nodes is set and we have nodes then return if all nodes are 'accepted'
+ if self.nodes is not None and len(self.nodes) > 0:
+ return all([node['accepted'] or node['denied'] for node in self.nodes])
+
+ # we are approving everything or we still have nodes outstanding
+ return False
+
+ def manage(self, action):
+ '''run openshift oc adm ca create-server-cert cmd and store results into self.nodes
+
+ we attempt to verify if the node is one that was given to us to accept.
+
+ action - (allow | deny)
+ '''
+
+ results = []
+ # There are 2 types of requests:
+ # - node-bootstrapper-client-ip-172-31-51-246-ec2-internal
+ # The client request allows the client to talk to the api/controller
+ # - node-bootstrapper-server-ip-172-31-51-246-ec2-internal
+ # The server request allows the server to join the cluster
+ # Here we need to determine how to approve/deny
+ # we should query the csrs and verify they are from the nodes we thought
+ for csr in self.csrs:
+ node = self.match_node(csr)
+ # oc adm certificate <approve|deny> csr
+ # there are 3 known states: Denied, Aprroved, {}
+ # verify something is needed by OCcsr.action_needed
+ # if approve_all, then do it
+ # if you passed in nodes, you must have a node that matches
+ if self.approve_all or (node and OCcsr.action_needed(csr, action)):
+ result = self.openshift_cmd(['certificate', action, csr['metadata']['name']], oadm=True)
+ # client should have service account name in username field
+ # server should have node name in username field
+ if node and csr['metadata']['name'] not in node['csrs']:
+ node['csrs'][csr['metadata']['name']] = csr
+
+ # accept node in cluster
+ if node['name'] in csr['spec']['username']:
+ node['accepted'] = True
+
+ results.append(result)
+
+ return results
+
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+
+ client = OCcsr(params['nodes'],
+ params['approve_all'],
+ params['service_account'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = client.get()
+
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if state in ['approve', 'deny']:
+ if check_mode:
+ return {'changed': True,
+ 'msg': "CHECK_MODE: Would have {} the certificate.".format(params['state']),
+ 'state': state}
+
+ all_results = []
+ finished = False
+ timeout = False
+ import time
+ # loop for timeout or block until all nodes pass
+ ctr = 0
+ while True:
+
+ all_results.extend(client.manage(params['state']))
+ if client.finished():
+ finished = True
+ break
+
+ if params['timeout'] == 0:
+ if not params['approve_all']:
+ ctr = 0
+
+ if ctr * 2 > params['timeout']:
+ timeout = True
+ break
+
+ # This provides time for the nodes to send their csr requests between approvals
+ time.sleep(2)
+
+ ctr += 1
+
+ for result in all_results:
+ if result['returncode'] != 0:
+ return {'failed': True, 'msg': all_results}
+
+ return dict(changed=len(all_results) > 0,
+ results=all_results,
+ nodes=client.nodes,
+ state=state,
+ finished=finished,
+ timeout=timeout)
+
+ return {'failed': True,
+ 'msg': 'Unknown state passed. %s' % state}
+
+
+# -*- -*- -*- End included fragment: class/oc_adm_csr.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_csr.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for approving certificate signing requests
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='approve', type='str',
+ choices=['approve', 'deny', 'list']),
+ debug=dict(default=False, type='bool'),
+ nodes=dict(default=None, type='list'),
+ timeout=dict(default=30, type='int'),
+ approve_all=dict(default=False, type='bool'),
+ service_account=dict(default='node-bootstrapper', type='str'),
+ ),
+ supports_check_mode=True,
+ mutually_exclusive=[['approve_all', 'nodes']],
+ )
+
+ if module.params['nodes'] == []:
+ module.fail_json(**dict(failed=True, msg='Please specify hosts.'))
+
+ rval = OCcsr.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_csr.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_manage_node.py b/roles/lib_openshift/library/oc_adm_manage_node.py
new file mode 100644
index 000000000..152f270ab
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_manage_node.py
@@ -0,0 +1,1671 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/manage_node -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_manage_node
+short_description: Module to manage openshift nodes
+description:
+ - Manage openshift nodes programmatically.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ node:
+ description:
+ - A list of the nodes being managed
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - The selector when filtering on node labels
+ required: false
+ default: None
+ aliases: []
+ pod_selector:
+ description:
+ - A selector when filtering on pod labels.
+ required: false
+ default: None
+ aliases: []
+ evacuate:
+ description:
+ - Remove all pods from a node.
+ required: false
+ default: False
+ aliases: []
+ schedulable:
+ description:
+ - whether or not openshift can schedule pods on this node
+ required: False
+ default: None
+ aliases: []
+ dry_run:
+ description:
+ - This shows the pods that would be migrated if evacuate were called
+ required: False
+ default: False
+ aliases: []
+ grace_period:
+ description:
+ - Grace period (seconds) for pods being deleted.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to attempt to force this action in openshift
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oadm manage-node --schedulable=true --selector=ops_node=new
+ oc_adm_manage_node:
+ selector: ops_node=new
+ schedulable: True
+ register: schedout
+
+- name: oadm manage-node my-k8s-node-5 --evacuate
+ oc_adm_manage_node:
+ node: my-k8s-node-5
+ evacuate: True
+ force: True
+'''
+
+# -*- -*- -*- End included fragment: doc/manage_node -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_manage_node.py -*- -*- -*-
+
+
+class ManageNodeException(Exception):
+ ''' manage-node exception class '''
+ pass
+
+
+class ManageNodeConfig(OpenShiftCLIConfig):
+ ''' ManageNodeConfig is a DTO for the manage-node command.'''
+ def __init__(self, kubeconfig, node_options):
+ super(ManageNodeConfig, self).__init__(None, None, kubeconfig, node_options)
+
+
+# pylint: disable=too-many-instance-attributes
+class ManageNode(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for ManageNode '''
+ super(ManageNode, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+
+ def evacuate(self):
+ ''' formulate the params and run oadm manage-node '''
+ return self._evacuate(node=self.config.config_options['node']['value'],
+ selector=self.config.config_options['selector']['value'],
+ pod_selector=self.config.config_options['pod_selector']['value'],
+ dry_run=self.config.config_options['dry_run']['value'],
+ grace_period=self.config.config_options['grace_period']['value'],
+ force=self.config.config_options['force']['value'],
+ )
+ def get_nodes(self, node=None, selector=''):
+ '''perform oc get node'''
+ _node = None
+ _sel = None
+ if node:
+ _node = node
+ if selector:
+ _sel = selector
+
+ results = self._get('node', name=_node, selector=_sel)
+ if results['returncode'] != 0:
+ return results
+
+ nodes = []
+ items = None
+ if results['results'][0]['kind'] == 'List':
+ items = results['results'][0]['items']
+ else:
+ items = results['results']
+
+ for node in items:
+ _node = {}
+ _node['name'] = node['metadata']['name']
+ _node['schedulable'] = True
+ if 'unschedulable' in node['spec']:
+ _node['schedulable'] = False
+ nodes.append(_node)
+
+ return nodes
+
+ def get_pods_from_node(self, node, pod_selector=None):
+ '''return pods for a node'''
+ results = self._list_pods(node=[node], pod_selector=pod_selector)
+
+ if results['returncode'] != 0:
+ return results
+
+ # When a selector or node is matched it is returned along with the json.
+ # We are going to split the results based on the regexp and then
+ # load the json for each matching node.
+ # Before we return we are going to loop over the results and pull out the node names.
+ # {'node': [pod, pod], 'node': [pod, pod]}
+ # 3.2 includes the following lines in stdout: "Listing matched pods on node:"
+ all_pods = []
+ if "Listing matched" in results['results']:
+ listing_match = re.compile('\n^Listing matched.*$\n', flags=re.MULTILINE)
+ pods = listing_match.split(results['results'])
+ for pod in pods:
+ if pod:
+ all_pods.extend(json.loads(pod)['items'])
+
+ # 3.3 specific
+ else:
+ # this is gross but I filed a bug...
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1381621
+ # build our own json from the output.
+ all_pods = json.loads(results['results'])['items']
+
+ return all_pods
+
+ def list_pods(self):
+ ''' run oadm manage-node --list-pods'''
+ _nodes = self.config.config_options['node']['value']
+ _selector = self.config.config_options['selector']['value']
+ _pod_selector = self.config.config_options['pod_selector']['value']
+
+ if not _nodes:
+ _nodes = self.get_nodes(selector=_selector)
+ else:
+ _nodes = [{'name': name} for name in _nodes]
+
+ all_pods = {}
+ for node in _nodes:
+ results = self.get_pods_from_node(node['name'], pod_selector=_pod_selector)
+ if isinstance(results, dict):
+ return results
+ all_pods[node['name']] = results
+
+ results = {}
+ results['nodes'] = all_pods
+ results['returncode'] = 0
+ return results
+
+ def schedulable(self):
+ '''oadm manage-node call for making nodes unschedulable'''
+ nodes = self.config.config_options['node']['value']
+ selector = self.config.config_options['selector']['value']
+
+ if not nodes:
+ nodes = self.get_nodes(selector=selector)
+ else:
+ tmp_nodes = []
+ for name in nodes:
+ tmp_result = self.get_nodes(name)
+ if isinstance(tmp_result, dict):
+ tmp_nodes.append(tmp_result)
+ continue
+ tmp_nodes.extend(tmp_result)
+ nodes = tmp_nodes
+
+ # This is a short circuit based on the way we fetch nodes.
+ # If node is a dict/list then we've already fetched them.
+ for node in nodes:
+ if isinstance(node, dict) and 'returncode' in node:
+ return {'results': nodes, 'returncode': node['returncode']}
+ if isinstance(node, list) and 'returncode' in node[0]:
+ return {'results': nodes, 'returncode': node[0]['returncode']}
+ # check all the nodes that were returned and verify they are:
+ # node['schedulable'] == self.config.config_options['schedulable']['value']
+ if any([node['schedulable'] != self.config.config_options['schedulable']['value'] for node in nodes]):
+
+ results = self._schedulable(node=self.config.config_options['node']['value'],
+ selector=self.config.config_options['selector']['value'],
+ schedulable=self.config.config_options['schedulable']['value'])
+
+ # 'NAME STATUS AGE\\nip-172-31-49-140.ec2.internal Ready 4h\\n' # E501
+ # normalize formatting with previous return objects
+ if results['results'].startswith('NAME'):
+ nodes = []
+ # removing header line and trailing new line character of node lines
+ for node_results in results['results'].split('\n')[1:-1]:
+ parts = node_results.split()
+ nodes.append({'name': parts[0], 'schedulable': parts[1] == 'Ready'})
+ results['nodes'] = nodes
+
+ return results
+
+ results = {}
+ results['returncode'] = 0
+ results['changed'] = False
+ results['nodes'] = nodes
+
+ return results
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+ nconfig = ManageNodeConfig(params['kubeconfig'],
+ {'node': {'value': params['node'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'pod_selector': {'value': params['pod_selector'], 'include': True},
+ 'schedulable': {'value': params['schedulable'], 'include': True},
+ 'list_pods': {'value': params['list_pods'], 'include': True},
+ 'evacuate': {'value': params['evacuate'], 'include': True},
+ 'dry_run': {'value': params['dry_run'], 'include': True},
+ 'force': {'value': params['force'], 'include': True},
+ 'grace_period': {'value': params['grace_period'], 'include': True},
+ })
+
+ oadm_mn = ManageNode(nconfig)
+ # Run the oadm manage-node commands
+ results = None
+ changed = False
+ if params['schedulable'] != None:
+ if check_mode:
+ # schedulable returns results after the fact.
+ # We need to redo how this works to support check_mode completely.
+ return {'changed': True, 'msg': 'CHECK_MODE: would have called schedulable.'}
+ results = oadm_mn.schedulable()
+ if 'changed' not in results:
+ changed = True
+
+ if params['evacuate']:
+ results = oadm_mn.evacuate()
+ changed = True
+ elif params['list_pods']:
+ results = oadm_mn.list_pods()
+
+ if not results or results['returncode'] != 0:
+ return {'failed': True, 'msg': results}
+
+ return {'changed': changed, 'results': results, 'state': "present"}
+
+# -*- -*- -*- End included fragment: class/oc_adm_manage_node.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_manage_node.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oadm module for manage-node
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ debug=dict(default=False, type='bool'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ node=dict(default=None, type='list'),
+ selector=dict(default=None, type='str'),
+ pod_selector=dict(default=None, type='str'),
+ schedulable=dict(default=None, type='bool'),
+ list_pods=dict(default=False, type='bool'),
+ evacuate=dict(default=False, type='bool'),
+ dry_run=dict(default=False, type='bool'),
+ force=dict(default=False, type='bool'),
+ grace_period=dict(default=None, type='int'),
+ ),
+ mutually_exclusive=[["selector", "node"], ['evacuate', 'list_pods'], ['list_pods', 'schedulable']],
+ required_one_of=[["node", "selector"]],
+
+ supports_check_mode=True,
+ )
+ results = ManageNode.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_manage_node.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_policy_group.py b/roles/lib_openshift/library/oc_adm_policy_group.py
new file mode 100644
index 000000000..3082f5890
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_policy_group.py
@@ -0,0 +1,2177 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/policy_group -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_policy_group
+short_description: Module to manage openshift policy for groups
+description:
+ - Manage openshift policy for groups.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace scope
+ required: false
+ default: None
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ group:
+ description:
+ - The name of the group
+ required: true
+ default: None
+ aliases: []
+ resource_kind:
+ description:
+ - The kind of policy to affect
+ required: true
+ default: None
+ choices: ["role", "cluster-role", "scc"]
+ aliases: []
+ resource_name:
+ description:
+ - The name of the policy
+ required: true
+ default: None
+ aliases: []
+ state:
+ description:
+ - Desired state of the policy
+ required: true
+ default: present
+ choices: ["present", "absent"]
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oc adm policy remove-scc-from-group an-scc agroup
+ oc_adm_policy_group:
+ group: agroup
+ resource_kind: scc
+ resource_name: an-scc
+ state: absent
+
+- name: oc adm policy add-cluster-role-to-group system:build-strategy-docker agroup
+ oc_adm_policy_group:
+ group: agroup
+ resource_kind: cluster-role
+ resource_name: system:build-strategy-docker
+ state: present
+'''
+
+# -*- -*- -*- End included fragment: doc/policy_group -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/rolebinding.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class RoleBindingConfig(object):
+ ''' Handle rolebinding config '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kubeconfig,
+ group_names=None,
+ role_ref=None,
+ subjects=None,
+ usernames=None):
+ ''' constructor for handling rolebinding options '''
+ self.kubeconfig = kubeconfig
+ self.name = name
+ self.namespace = namespace
+ self.group_names = group_names
+ self.role_ref = role_ref
+ self.subjects = subjects
+ self.usernames = usernames
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' create a default rolebinding as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'RoleBinding'
+ self.data['groupNames'] = self.group_names
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['roleRef'] = self.role_ref
+ self.data['subjects'] = self.subjects
+ self.data['userNames'] = self.usernames
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class RoleBinding(Yedit):
+ ''' Class to model a rolebinding openshift object'''
+ group_names_path = "groupNames"
+ role_ref_path = "roleRef"
+ subjects_path = "subjects"
+ user_names_path = "userNames"
+
+ kind = 'RoleBinding'
+
+ def __init__(self, content):
+ '''RoleBinding constructor'''
+ super(RoleBinding, self).__init__(content=content)
+ self._subjects = None
+ self._role_ref = None
+ self._group_names = None
+ self._user_names = None
+
+ @property
+ def subjects(self):
+ ''' subjects property '''
+ if self._subjects is None:
+ self._subjects = self.get_subjects()
+ return self._subjects
+
+ @subjects.setter
+ def subjects(self, data):
+ ''' subjects property setter'''
+ self._subjects = data
+
+ @property
+ def role_ref(self):
+ ''' role_ref property '''
+ if self._role_ref is None:
+ self._role_ref = self.get_role_ref()
+ return self._role_ref
+
+ @role_ref.setter
+ def role_ref(self, data):
+ ''' role_ref property setter'''
+ self._role_ref = data
+
+ @property
+ def group_names(self):
+ ''' group_names property '''
+ if self._group_names is None:
+ self._group_names = self.get_group_names()
+ return self._group_names
+
+ @group_names.setter
+ def group_names(self, data):
+ ''' group_names property setter'''
+ self._group_names = data
+
+ @property
+ def user_names(self):
+ ''' user_names property '''
+ if self._user_names is None:
+ self._user_names = self.get_user_names()
+ return self._user_names
+
+ @user_names.setter
+ def user_names(self, data):
+ ''' user_names property setter'''
+ self._user_names = data
+
+ def get_group_names(self):
+ ''' return groupNames '''
+ return self.get(RoleBinding.group_names_path) or []
+
+ def get_user_names(self):
+ ''' return usernames '''
+ return self.get(RoleBinding.user_names_path) or []
+
+ def get_role_ref(self):
+ ''' return role_ref '''
+ return self.get(RoleBinding.role_ref_path) or {}
+
+ def get_subjects(self):
+ ''' return subjects '''
+ return self.get(RoleBinding.subjects_path) or []
+
+ #### ADD #####
+ def add_subject(self, inc_subject):
+ ''' add a subject '''
+ if self.subjects:
+ # pylint: disable=no-member
+ self.subjects.append(inc_subject)
+ else:
+ self.put(RoleBinding.subjects_path, [inc_subject])
+
+ return True
+
+ def add_role_ref(self, inc_role_ref):
+ ''' add a role_ref '''
+ if not self.role_ref:
+ self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
+ return True
+
+ return False
+
+ def add_group_names(self, inc_group_names):
+ ''' add a group_names '''
+ if self.group_names:
+ # pylint: disable=no-member
+ self.group_names.append(inc_group_names)
+ else:
+ self.put(RoleBinding.group_names_path, [inc_group_names])
+
+ return True
+
+ def add_user_name(self, inc_user_name):
+ ''' add a username '''
+ if self.user_names:
+ # pylint: disable=no-member
+ self.user_names.append(inc_user_name)
+ else:
+ self.put(RoleBinding.user_names_path, [inc_user_name])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_subject(self, inc_subject):
+ ''' remove a subject '''
+ try:
+ # pylint: disable=no-member
+ self.subjects.remove(inc_subject)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_role_ref(self, inc_role_ref):
+ ''' remove a role_ref '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref:
+ del self.role_ref['name']
+ return True
+
+ return False
+
+ def remove_group_name(self, inc_group_name):
+ ''' remove a groupname '''
+ try:
+ # pylint: disable=no-member
+ self.group_names.remove(inc_group_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_user_name(self, inc_user_name):
+ ''' remove a username '''
+ try:
+ # pylint: disable=no-member
+ self.user_names.remove(inc_user_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_subject(self, inc_subject):
+ ''' update a subject '''
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return self.add_subject(inc_subject)
+
+ self.subjects[index] = inc_subject
+
+ return True
+
+ def update_group_name(self, inc_group_name):
+ ''' update a groupname '''
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return self.add_group_names(inc_group_name)
+
+ self.group_names[index] = inc_group_name
+
+ return True
+
+ def update_user_name(self, inc_user_name):
+ ''' update a username '''
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return self.add_user_name(inc_user_name)
+
+ self.user_names[index] = inc_user_name
+
+ return True
+
+ def update_role_ref(self, inc_role_ref):
+ ''' update a role_ref '''
+ self.role_ref['name'] = inc_role_ref
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_subject(self, inc_subject):
+ ''' find a subject '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group_name(self, inc_group_name):
+ ''' find a group_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_user_name(self, inc_user_name):
+ ''' find a user_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_role_ref(self, inc_role_ref):
+ ''' find a user_name '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
+ return self.role_ref
+
+ return None
+
+# -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/scc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class SecurityContextConstraintsConfig(object):
+ ''' Handle scc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ kubeconfig,
+ options=None,
+ fs_group='MustRunAs',
+ default_add_capabilities=None,
+ groups=None,
+ priority=None,
+ required_drop_capabilities=None,
+ run_as_user='MustRunAsRange',
+ se_linux_context='MustRunAs',
+ supplemental_groups='RunAsAny',
+ users=None,
+ annotations=None):
+ ''' constructor for handling scc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.options = options
+ self.fs_group = fs_group
+ self.default_add_capabilities = default_add_capabilities
+ self.groups = groups
+ self.priority = priority
+ self.required_drop_capabilities = required_drop_capabilities
+ self.run_as_user = run_as_user
+ self.se_linux_context = se_linux_context
+ self.supplemental_groups = supplemental_groups
+ self.users = users
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a scc dict '''
+ # allow options
+ if self.options:
+ for key, value in self.options.items():
+ self.data[key] = value
+ else:
+ self.data['allowHostDirVolumePlugin'] = False
+ self.data['allowHostIPC'] = False
+ self.data['allowHostNetwork'] = False
+ self.data['allowHostPID'] = False
+ self.data['allowHostPorts'] = False
+ self.data['allowPrivilegedContainer'] = False
+ self.data['allowedCapabilities'] = None
+
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'SecurityContextConstraints'
+ # defaultAddCapabilities
+ self.data['defaultAddCapabilities'] = self.default_add_capabilities
+ # fsGroup
+ self.data['fsGroup']['type'] = self.fs_group
+ # groups
+ self.data['groups'] = []
+ if self.groups:
+ self.data['groups'] = self.groups
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ if self.annotations:
+ for key, value in self.annotations.items():
+ self.data['metadata'][key] = value
+ # priority
+ self.data['priority'] = self.priority
+ # requiredDropCapabilities
+ self.data['requiredDropCapabilities'] = self.required_drop_capabilities
+ # runAsUser
+ self.data['runAsUser'] = {'type': self.run_as_user}
+ # seLinuxContext
+ self.data['seLinuxContext'] = {'type': self.se_linux_context}
+ # supplementalGroups
+ self.data['supplementalGroups'] = {'type': self.supplemental_groups}
+ # users
+ self.data['users'] = []
+ if self.users:
+ self.data['users'] = self.users
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods,no-member
+class SecurityContextConstraints(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ default_add_capabilities_path = "defaultAddCapabilities"
+ fs_group_path = "fsGroup"
+ groups_path = "groups"
+ priority_path = "priority"
+ required_drop_capabilities_path = "requiredDropCapabilities"
+ run_as_user_path = "runAsUser"
+ se_linux_context_path = "seLinuxContext"
+ supplemental_groups_path = "supplementalGroups"
+ users_path = "users"
+ kind = 'SecurityContextConstraints'
+
+ def __init__(self, content):
+ '''SecurityContextConstraints constructor'''
+ super(SecurityContextConstraints, self).__init__(content=content)
+ self._users = None
+ self._groups = None
+
+ @property
+ def users(self):
+ ''' users property getter '''
+ if self._users is None:
+ self._users = self.get_users()
+ return self._users
+
+ @property
+ def groups(self):
+ ''' groups property getter '''
+ if self._groups is None:
+ self._groups = self.get_groups()
+ return self._groups
+
+ @users.setter
+ def users(self, data):
+ ''' users property setter'''
+ self._users = data
+
+ @groups.setter
+ def groups(self, data):
+ ''' groups property setter'''
+ self._groups = data
+
+ def get_users(self):
+ '''get scc users'''
+ return self.get(SecurityContextConstraints.users_path) or []
+
+ def get_groups(self):
+ '''get scc groups'''
+ return self.get(SecurityContextConstraints.groups_path) or []
+
+ def add_user(self, inc_user):
+ ''' add a user '''
+ if self.users:
+ self.users.append(inc_user)
+ else:
+ self.put(SecurityContextConstraints.users_path, [inc_user])
+
+ return True
+
+ def add_group(self, inc_group):
+ ''' add a group '''
+ if self.groups:
+ self.groups.append(inc_group)
+ else:
+ self.put(SecurityContextConstraints.groups_path, [inc_group])
+
+ return True
+
+ def remove_user(self, inc_user):
+ ''' remove a user '''
+ try:
+ self.users.remove(inc_user)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_group(self, inc_group):
+ ''' remove a group '''
+ try:
+ self.groups.remove(inc_group)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def update_user(self, inc_user):
+ ''' update a user '''
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return self.add_user(inc_user)
+
+ self.users[index] = inc_user
+
+ return True
+
+ def update_group(self, inc_group):
+ ''' update a group '''
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return self.add_group(inc_group)
+
+ self.groups[index] = inc_group
+
+ return True
+
+ def find_user(self, inc_user):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group(self, inc_group):
+ ''' find a group '''
+ index = None
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return index
+
+ return index
+
+# -*- -*- -*- End included fragment: lib/scc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_policy_group.py -*- -*- -*-
+
+
+class PolicyGroupException(Exception):
+ ''' PolicyGroup exception'''
+ pass
+
+
+class PolicyGroupConfig(OpenShiftCLIConfig):
+ ''' PolicyGroupConfig is a DTO for group related policy. '''
+ def __init__(self, namespace, kubeconfig, policy_options):
+ super(PolicyGroupConfig, self).__init__(policy_options['name']['value'],
+ namespace, kubeconfig, policy_options)
+ self.kind = self.get_kind()
+ self.namespace = namespace
+
+ def get_kind(self):
+ ''' return the kind we are working with '''
+ if self.config_options['resource_kind']['value'] == 'role':
+ return 'rolebinding'
+ elif self.config_options['resource_kind']['value'] == 'cluster-role':
+ return 'clusterrolebinding'
+ elif self.config_options['resource_kind']['value'] == 'scc':
+ return 'scc'
+
+ return None
+
+
+# pylint: disable=too-many-return-statements
+class PolicyGroup(OpenShiftCLI):
+ ''' Class to handle attaching policies to users '''
+
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for PolicyGroup '''
+ super(PolicyGroup, self).__init__(config.namespace, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+ self._rolebinding = None
+ self._scc = None
+ self._cluster_role_bindings = None
+ self._role_bindings = None
+
+ @property
+ def rolebindings(self):
+ if self._role_bindings is None:
+ results = self._get('rolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve rolebindings')
+ self._role_bindings = results['results'][0]['items']
+
+ return self._role_bindings
+
+ @property
+ def clusterrolebindings(self):
+ if self._cluster_role_bindings is None:
+ results = self._get('clusterrolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve clusterrolebindings')
+ self._cluster_role_bindings = results['results'][0]['items']
+
+ return self._cluster_role_bindings
+
+ @property
+ def role_binding(self):
+ ''' role_binding getter '''
+ return self._rolebinding
+
+ @role_binding.setter
+ def role_binding(self, binding):
+ ''' role_binding setter '''
+ self._rolebinding = binding
+
+ @property
+ def security_context_constraint(self):
+ ''' security_context_constraint getter '''
+ return self._scc
+
+ @security_context_constraint.setter
+ def security_context_constraint(self, scc):
+ ''' security_context_constraint setter '''
+ self._scc = scc
+
+ def get(self):
+ '''fetch the desired kind'''
+ resource_name = self.config.config_options['name']['value']
+ if resource_name == 'cluster-reader':
+ resource_name += 's'
+
+ # oc adm policy add-... creates policy bindings with the name
+ # "[resource_name]-binding", however some bindings in the system
+ # simply use "[resource_name]". So try both.
+
+ results = self._get(self.config.kind, resource_name)
+ if results['returncode'] == 0:
+ return results
+
+ # Now try -binding naming convention
+ return self._get(self.config.kind, resource_name + "-binding")
+
+ def exists_role_binding(self):
+ ''' return whether role_binding exists '''
+ bindings = None
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ bindings = self.clusterrolebindings
+ else:
+ bindings = self.rolebindings
+
+ if bindings is None:
+ return False
+
+ for binding in bindings:
+ if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \
+ binding['groupNames'] is not None and \
+ self.config.config_options['group']['value'] in binding['groupNames']:
+ self.role_binding = binding
+ return True
+
+ return False
+
+ def exists_scc(self):
+ ''' return whether scc exists '''
+ results = self.get()
+ if results['returncode'] == 0:
+ self.security_context_constraint = SecurityContextConstraints(results['results'][0])
+
+ if self.security_context_constraint.find_group(self.config.config_options['group']['value']) != None:
+ return True
+
+ return False
+
+ return results
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'scc':
+ return self.exists_scc()
+
+ return False
+
+ def perform(self):
+ '''perform action on resource'''
+ cmd = ['policy',
+ self.config.config_options['action']['value'],
+ self.config.config_options['name']['value'],
+ self.config.config_options['group']['value']]
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ state = params['state']
+
+ action = None
+ if state == 'present':
+ action = 'add-' + params['resource_kind'] + '-to-group'
+ else:
+ action = 'remove-' + params['resource_kind'] + '-from-group'
+
+ nconfig = PolicyGroupConfig(params['namespace'],
+ params['kubeconfig'],
+ {'action': {'value': action, 'include': False},
+ 'group': {'value': params['group'], 'include': False},
+ 'resource_kind': {'value': params['resource_kind'], 'include': False},
+ 'name': {'value': params['resource_name'], 'include': False},
+ })
+
+ policygroup = PolicyGroup(nconfig, params['debug'])
+
+ # Run the oc adm policy group related command
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not policygroup.exists():
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a delete.'}
+
+ api_rval = policygroup.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results' : api_rval, state:'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ results = policygroup.exists()
+ if isinstance(results, dict) and 'returncode' in results and results['returncode'] != 0:
+ return {'msg': results}
+
+ if not results:
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a create.'}
+
+ api_rval = policygroup.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: 'present'}
+
+ return {'changed': False, state: 'present'}
+
+ return {'failed': True, 'changed': False, 'results': 'Unknown state passed. %s' % state, state: 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_adm_policy_group.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_policy_group.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc adm module for group policy
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ resource_name=dict(required=True, type='str'),
+ namespace=dict(default='default', type='str'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+
+ group=dict(required=True, type='str'),
+ resource_kind=dict(required=True, choices=['role', 'cluster-role', 'scc'], type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = PolicyGroup.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_policy_group.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_policy_user.py b/roles/lib_openshift/library/oc_adm_policy_user.py
new file mode 100644
index 000000000..1ceaf5d0d
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_policy_user.py
@@ -0,0 +1,2171 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/policy_user -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_policy_user
+short_description: Module to manage openshift policy for users
+description:
+ - Manage openshift policy for users.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace scope
+ required: false
+ default: None
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ user:
+ description:
+ - The name of the user
+ required: true
+ default: None
+ aliases: []
+ resource_kind:
+ description:
+ - The kind of policy to affect
+ required: true
+ default: None
+ choices: ["role", "cluster-role", "scc"]
+ aliases: []
+ resource_name:
+ description:
+ - The name of the policy
+ required: true
+ default: None
+ aliases: []
+ state:
+ description:
+ - Desired state of the policy
+ required: true
+ default: present
+ choices: ["present", "absent"]
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oc adm policy remove-scc-from-user an-scc ausername
+ oc_adm_policy_user:
+ user: ausername
+ resource_kind: scc
+ resource_name: an-scc
+ state: absent
+
+- name: oc adm policy add-cluster-role-to-user system:build-strategy-docker ausername
+ oc_adm_policy_user:
+ user: ausername
+ resource_kind: cluster-role
+ resource_name: system:build-strategy-docker
+ state: present
+'''
+
+# -*- -*- -*- End included fragment: doc/policy_user -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/rolebinding.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class RoleBindingConfig(object):
+ ''' Handle rolebinding config '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kubeconfig,
+ group_names=None,
+ role_ref=None,
+ subjects=None,
+ usernames=None):
+ ''' constructor for handling rolebinding options '''
+ self.kubeconfig = kubeconfig
+ self.name = name
+ self.namespace = namespace
+ self.group_names = group_names
+ self.role_ref = role_ref
+ self.subjects = subjects
+ self.usernames = usernames
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' create a default rolebinding as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'RoleBinding'
+ self.data['groupNames'] = self.group_names
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['roleRef'] = self.role_ref
+ self.data['subjects'] = self.subjects
+ self.data['userNames'] = self.usernames
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class RoleBinding(Yedit):
+ ''' Class to model a rolebinding openshift object'''
+ group_names_path = "groupNames"
+ role_ref_path = "roleRef"
+ subjects_path = "subjects"
+ user_names_path = "userNames"
+
+ kind = 'RoleBinding'
+
+ def __init__(self, content):
+ '''RoleBinding constructor'''
+ super(RoleBinding, self).__init__(content=content)
+ self._subjects = None
+ self._role_ref = None
+ self._group_names = None
+ self._user_names = None
+
+ @property
+ def subjects(self):
+ ''' subjects property '''
+ if self._subjects is None:
+ self._subjects = self.get_subjects()
+ return self._subjects
+
+ @subjects.setter
+ def subjects(self, data):
+ ''' subjects property setter'''
+ self._subjects = data
+
+ @property
+ def role_ref(self):
+ ''' role_ref property '''
+ if self._role_ref is None:
+ self._role_ref = self.get_role_ref()
+ return self._role_ref
+
+ @role_ref.setter
+ def role_ref(self, data):
+ ''' role_ref property setter'''
+ self._role_ref = data
+
+ @property
+ def group_names(self):
+ ''' group_names property '''
+ if self._group_names is None:
+ self._group_names = self.get_group_names()
+ return self._group_names
+
+ @group_names.setter
+ def group_names(self, data):
+ ''' group_names property setter'''
+ self._group_names = data
+
+ @property
+ def user_names(self):
+ ''' user_names property '''
+ if self._user_names is None:
+ self._user_names = self.get_user_names()
+ return self._user_names
+
+ @user_names.setter
+ def user_names(self, data):
+ ''' user_names property setter'''
+ self._user_names = data
+
+ def get_group_names(self):
+ ''' return groupNames '''
+ return self.get(RoleBinding.group_names_path) or []
+
+ def get_user_names(self):
+ ''' return usernames '''
+ return self.get(RoleBinding.user_names_path) or []
+
+ def get_role_ref(self):
+ ''' return role_ref '''
+ return self.get(RoleBinding.role_ref_path) or {}
+
+ def get_subjects(self):
+ ''' return subjects '''
+ return self.get(RoleBinding.subjects_path) or []
+
+ #### ADD #####
+ def add_subject(self, inc_subject):
+ ''' add a subject '''
+ if self.subjects:
+ # pylint: disable=no-member
+ self.subjects.append(inc_subject)
+ else:
+ self.put(RoleBinding.subjects_path, [inc_subject])
+
+ return True
+
+ def add_role_ref(self, inc_role_ref):
+ ''' add a role_ref '''
+ if not self.role_ref:
+ self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
+ return True
+
+ return False
+
+ def add_group_names(self, inc_group_names):
+ ''' add a group_names '''
+ if self.group_names:
+ # pylint: disable=no-member
+ self.group_names.append(inc_group_names)
+ else:
+ self.put(RoleBinding.group_names_path, [inc_group_names])
+
+ return True
+
+ def add_user_name(self, inc_user_name):
+ ''' add a username '''
+ if self.user_names:
+ # pylint: disable=no-member
+ self.user_names.append(inc_user_name)
+ else:
+ self.put(RoleBinding.user_names_path, [inc_user_name])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_subject(self, inc_subject):
+ ''' remove a subject '''
+ try:
+ # pylint: disable=no-member
+ self.subjects.remove(inc_subject)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_role_ref(self, inc_role_ref):
+ ''' remove a role_ref '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref:
+ del self.role_ref['name']
+ return True
+
+ return False
+
+ def remove_group_name(self, inc_group_name):
+ ''' remove a groupname '''
+ try:
+ # pylint: disable=no-member
+ self.group_names.remove(inc_group_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_user_name(self, inc_user_name):
+ ''' remove a username '''
+ try:
+ # pylint: disable=no-member
+ self.user_names.remove(inc_user_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_subject(self, inc_subject):
+ ''' update a subject '''
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return self.add_subject(inc_subject)
+
+ self.subjects[index] = inc_subject
+
+ return True
+
+ def update_group_name(self, inc_group_name):
+ ''' update a groupname '''
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return self.add_group_names(inc_group_name)
+
+ self.group_names[index] = inc_group_name
+
+ return True
+
+ def update_user_name(self, inc_user_name):
+ ''' update a username '''
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return self.add_user_name(inc_user_name)
+
+ self.user_names[index] = inc_user_name
+
+ return True
+
+ def update_role_ref(self, inc_role_ref):
+ ''' update a role_ref '''
+ self.role_ref['name'] = inc_role_ref
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_subject(self, inc_subject):
+ ''' find a subject '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group_name(self, inc_group_name):
+ ''' find a group_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_user_name(self, inc_user_name):
+ ''' find a user_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_role_ref(self, inc_role_ref):
+ ''' find a user_name '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
+ return self.role_ref
+
+ return None
+
+# -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/scc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class SecurityContextConstraintsConfig(object):
+ ''' Handle scc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ kubeconfig,
+ options=None,
+ fs_group='MustRunAs',
+ default_add_capabilities=None,
+ groups=None,
+ priority=None,
+ required_drop_capabilities=None,
+ run_as_user='MustRunAsRange',
+ se_linux_context='MustRunAs',
+ supplemental_groups='RunAsAny',
+ users=None,
+ annotations=None):
+ ''' constructor for handling scc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.options = options
+ self.fs_group = fs_group
+ self.default_add_capabilities = default_add_capabilities
+ self.groups = groups
+ self.priority = priority
+ self.required_drop_capabilities = required_drop_capabilities
+ self.run_as_user = run_as_user
+ self.se_linux_context = se_linux_context
+ self.supplemental_groups = supplemental_groups
+ self.users = users
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a scc dict '''
+ # allow options
+ if self.options:
+ for key, value in self.options.items():
+ self.data[key] = value
+ else:
+ self.data['allowHostDirVolumePlugin'] = False
+ self.data['allowHostIPC'] = False
+ self.data['allowHostNetwork'] = False
+ self.data['allowHostPID'] = False
+ self.data['allowHostPorts'] = False
+ self.data['allowPrivilegedContainer'] = False
+ self.data['allowedCapabilities'] = None
+
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'SecurityContextConstraints'
+ # defaultAddCapabilities
+ self.data['defaultAddCapabilities'] = self.default_add_capabilities
+ # fsGroup
+ self.data['fsGroup']['type'] = self.fs_group
+ # groups
+ self.data['groups'] = []
+ if self.groups:
+ self.data['groups'] = self.groups
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ if self.annotations:
+ for key, value in self.annotations.items():
+ self.data['metadata'][key] = value
+ # priority
+ self.data['priority'] = self.priority
+ # requiredDropCapabilities
+ self.data['requiredDropCapabilities'] = self.required_drop_capabilities
+ # runAsUser
+ self.data['runAsUser'] = {'type': self.run_as_user}
+ # seLinuxContext
+ self.data['seLinuxContext'] = {'type': self.se_linux_context}
+ # supplementalGroups
+ self.data['supplementalGroups'] = {'type': self.supplemental_groups}
+ # users
+ self.data['users'] = []
+ if self.users:
+ self.data['users'] = self.users
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods,no-member
+class SecurityContextConstraints(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ default_add_capabilities_path = "defaultAddCapabilities"
+ fs_group_path = "fsGroup"
+ groups_path = "groups"
+ priority_path = "priority"
+ required_drop_capabilities_path = "requiredDropCapabilities"
+ run_as_user_path = "runAsUser"
+ se_linux_context_path = "seLinuxContext"
+ supplemental_groups_path = "supplementalGroups"
+ users_path = "users"
+ kind = 'SecurityContextConstraints'
+
+ def __init__(self, content):
+ '''SecurityContextConstraints constructor'''
+ super(SecurityContextConstraints, self).__init__(content=content)
+ self._users = None
+ self._groups = None
+
+ @property
+ def users(self):
+ ''' users property getter '''
+ if self._users is None:
+ self._users = self.get_users()
+ return self._users
+
+ @property
+ def groups(self):
+ ''' groups property getter '''
+ if self._groups is None:
+ self._groups = self.get_groups()
+ return self._groups
+
+ @users.setter
+ def users(self, data):
+ ''' users property setter'''
+ self._users = data
+
+ @groups.setter
+ def groups(self, data):
+ ''' groups property setter'''
+ self._groups = data
+
+ def get_users(self):
+ '''get scc users'''
+ return self.get(SecurityContextConstraints.users_path) or []
+
+ def get_groups(self):
+ '''get scc groups'''
+ return self.get(SecurityContextConstraints.groups_path) or []
+
+ def add_user(self, inc_user):
+ ''' add a user '''
+ if self.users:
+ self.users.append(inc_user)
+ else:
+ self.put(SecurityContextConstraints.users_path, [inc_user])
+
+ return True
+
+ def add_group(self, inc_group):
+ ''' add a group '''
+ if self.groups:
+ self.groups.append(inc_group)
+ else:
+ self.put(SecurityContextConstraints.groups_path, [inc_group])
+
+ return True
+
+ def remove_user(self, inc_user):
+ ''' remove a user '''
+ try:
+ self.users.remove(inc_user)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_group(self, inc_group):
+ ''' remove a group '''
+ try:
+ self.groups.remove(inc_group)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def update_user(self, inc_user):
+ ''' update a user '''
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return self.add_user(inc_user)
+
+ self.users[index] = inc_user
+
+ return True
+
+ def update_group(self, inc_group):
+ ''' update a group '''
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return self.add_group(inc_group)
+
+ self.groups[index] = inc_group
+
+ return True
+
+ def find_user(self, inc_user):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group(self, inc_group):
+ ''' find a group '''
+ index = None
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return index
+
+ return index
+
+# -*- -*- -*- End included fragment: lib/scc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_policy_user.py -*- -*- -*-
+
+
+class PolicyUserException(Exception):
+ ''' PolicyUser exception'''
+ pass
+
+
+class PolicyUserConfig(OpenShiftCLIConfig):
+ ''' PolicyUserConfig is a DTO for user related policy. '''
+ def __init__(self, namespace, kubeconfig, policy_options):
+ super(PolicyUserConfig, self).__init__(policy_options['name']['value'],
+ namespace, kubeconfig, policy_options)
+ self.kind = self.get_kind()
+ self.namespace = namespace
+
+ def get_kind(self):
+ ''' return the kind we are working with '''
+ if self.config_options['resource_kind']['value'] == 'role':
+ return 'rolebinding'
+ elif self.config_options['resource_kind']['value'] == 'cluster-role':
+ return 'clusterrolebinding'
+ elif self.config_options['resource_kind']['value'] == 'scc':
+ return 'scc'
+
+ return None
+
+
+# pylint: disable=too-many-return-statements
+class PolicyUser(OpenShiftCLI):
+ ''' Class to handle attaching policies to users '''
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for PolicyUser '''
+ super(PolicyUser, self).__init__(config.namespace, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+ self._rolebinding = None
+ self._scc = None
+ self._cluster_role_bindings = None
+ self._role_bindings = None
+
+ @property
+ def rolebindings(self):
+ if self._role_bindings is None:
+ results = self._get('rolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve rolebindings')
+ self._role_bindings = results['results'][0]['items']
+
+ return self._role_bindings
+
+ @property
+ def clusterrolebindings(self):
+ if self._cluster_role_bindings is None:
+ results = self._get('clusterrolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve clusterrolebindings')
+ self._cluster_role_bindings = results['results'][0]['items']
+
+ return self._cluster_role_bindings
+
+ @property
+ def role_binding(self):
+ ''' role_binding property '''
+ return self._rolebinding
+
+ @role_binding.setter
+ def role_binding(self, binding):
+ ''' setter for role_binding property '''
+ self._rolebinding = binding
+
+ @property
+ def security_context_constraint(self):
+ ''' security_context_constraint property '''
+ return self._scc
+
+ @security_context_constraint.setter
+ def security_context_constraint(self, scc):
+ ''' setter for security_context_constraint property '''
+ self._scc = scc
+
+ def get(self):
+ '''fetch the desired kind
+
+ This is only used for scc objects.
+ The {cluster}rolebindings happen in exists.
+ '''
+ resource_name = self.config.config_options['name']['value']
+ if resource_name == 'cluster-reader':
+ resource_name += 's'
+
+ return self._get(self.config.kind, resource_name)
+
+ def exists_role_binding(self):
+ ''' return whether role_binding exists '''
+ bindings = None
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ bindings = self.clusterrolebindings
+ else:
+ bindings = self.rolebindings
+
+ if bindings is None:
+ return False
+
+ for binding in bindings:
+ if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \
+ binding['userNames'] is not None and \
+ self.config.config_options['user']['value'] in binding['userNames']:
+ self.role_binding = binding
+ return True
+
+ return False
+
+ def exists_scc(self):
+ ''' return whether scc exists '''
+ results = self.get()
+ if results['returncode'] == 0:
+ self.security_context_constraint = SecurityContextConstraints(results['results'][0])
+
+ if self.security_context_constraint.find_user(self.config.config_options['user']['value']) != None:
+ return True
+
+ return False
+
+ return results
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'scc':
+ return self.exists_scc()
+
+ return False
+
+ def perform(self):
+ '''perform action on resource'''
+ cmd = ['policy',
+ self.config.config_options['action']['value'],
+ self.config.config_options['name']['value'],
+ self.config.config_options['user']['value']]
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ state = params['state']
+
+ action = None
+ if state == 'present':
+ action = 'add-' + params['resource_kind'] + '-to-user'
+ else:
+ action = 'remove-' + params['resource_kind'] + '-from-user'
+
+ nconfig = PolicyUserConfig(params['namespace'],
+ params['kubeconfig'],
+ {'action': {'value': action, 'include': False},
+ 'user': {'value': params['user'], 'include': False},
+ 'resource_kind': {'value': params['resource_kind'], 'include': False},
+ 'name': {'value': params['resource_name'], 'include': False},
+ })
+
+ policyuser = PolicyUser(nconfig, params['debug'])
+
+ # Run the oc adm policy user related command
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not policyuser.exists():
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a delete.'}
+
+ api_rval = policyuser.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results' : api_rval, state:'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ results = policyuser.exists()
+ if isinstance(results, dict) and 'returncode' in results and results['returncode'] != 0:
+ return {'msg': results}
+
+ if not results:
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a create.'}
+
+ api_rval = policyuser.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: 'present'}
+
+ return {'changed': False, state: 'present'}
+
+ return {'failed': True, 'changed': False, 'results': 'Unknown state passed. %s' % state, state: 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_adm_policy_user.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_policy_user.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc adm module for user policy
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ resource_name=dict(required=True, type='str'),
+ namespace=dict(default='default', type='str'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+
+ user=dict(required=True, type='str'),
+ resource_kind=dict(required=True, choices=['role', 'cluster-role', 'scc'], type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = PolicyUser.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_policy_user.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_registry.py b/roles/lib_openshift/library/oc_adm_registry.py
new file mode 100644
index 000000000..0771aa5a5
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_registry.py
@@ -0,0 +1,2746 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/registry -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_registry
+short_description: Module to manage openshift registry
+description:
+ - Manage openshift registry programmatically.
+options:
+ state:
+ description:
+ - The desired action when managing openshift registry
+ - present - update or create the registry
+ - absent - tear down the registry service and deploymentconfig
+ - list - returns the current representiation of a registry
+ required: false
+ default: False
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - The name of the registry
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The selector when filtering on node labels
+ required: false
+ default: None
+ aliases: []
+ images:
+ description:
+ - The image to base this registry on - ${component} will be replaced with --type
+ required: 'openshift3/ose-${component}:${version}'
+ default: None
+ aliases: []
+ latest_images:
+ description:
+ - If true, attempt to use the latest image for the registry instead of the latest release.
+ required: false
+ default: False
+ aliases: []
+ labels:
+ description:
+ - A set of labels to uniquely identify the registry and its components.
+ required: false
+ default: None
+ aliases: []
+ enforce_quota:
+ description:
+ - If set, the registry will refuse to write blobs if they exceed quota limits
+ required: False
+ default: False
+ aliases: []
+ mount_host:
+ description:
+ - If set, the registry volume will be created as a host-mount at this path.
+ required: False
+ default: False
+ aliases: []
+ ports:
+ description:
+ - A comma delimited list of ports or port pairs to expose on the registry pod. The default is set for 5000.
+ required: False
+ default: [5000]
+ aliases: []
+ replicas:
+ description:
+ - The replication factor of the registry; commonly 2 when high availability is desired.
+ required: False
+ default: 1
+ aliases: []
+ selector:
+ description:
+ - Selector used to filter nodes on deployment. Used to run registries on a specific set of nodes.
+ required: False
+ default: None
+ aliases: []
+ service_account:
+ description:
+ - Name of the service account to use to run the registry pod.
+ required: False
+ default: 'registry'
+ aliases: []
+ tls_certificate:
+ description:
+ - An optional path to a PEM encoded certificate (which may contain the private key) for serving over TLS
+ required: false
+ default: None
+ aliases: []
+ tls_key:
+ description:
+ - An optional path to a PEM encoded private key for serving over TLS
+ required: false
+ default: None
+ aliases: []
+ volume_mounts:
+ description:
+ - The volume mounts for the registry.
+ required: false
+ default: None
+ aliases: []
+ daemonset:
+ description:
+ - Use a daemonset instead of a deployment config.
+ required: false
+ default: False
+ aliases: []
+ edits:
+ description:
+ - A list of modifications to make on the deploymentconfig
+ required: false
+ default: None
+ aliases: []
+ env_vars:
+ description:
+ - A dictionary of modifications to make on the deploymentconfig. e.g. FOO: BAR
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Force a registry update.
+ required: false
+ default: False
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create a secure registry
+ oc_adm_registry:
+ name: docker-registry
+ service_account: registry
+ replicas: 2
+ namespace: default
+ selector: type=infra
+ images: "registry.ops.openshift.com/openshift3/ose-${component}:${version}"
+ env_vars:
+ REGISTRY_CONFIGURATION_PATH: /etc/registryconfig/config.yml
+ REGISTRY_HTTP_TLS_CERTIFICATE: /etc/secrets/registry.crt
+ REGISTRY_HTTP_TLS_KEY: /etc/secrets/registry.key
+ REGISTRY_HTTP_SECRET: supersecret
+ volume_mounts:
+ - path: /etc/secrets
+ name: dockercerts
+ type: secret
+ secret_name: registry-secret
+ - path: /etc/registryconfig
+ name: dockersecrets
+ type: secret
+ secret_name: docker-registry-config
+ edits:
+ - key: spec.template.spec.containers[0].livenessProbe.httpGet.scheme
+ value: HTTPS
+ action: put
+ - key: spec.template.spec.containers[0].readinessProbe.httpGet.scheme
+ value: HTTPS
+ action: put
+ - key: spec.strategy.rollingParams
+ value:
+ intervalSeconds: 1
+ maxSurge: 50%
+ maxUnavailable: 50%
+ timeoutSeconds: 600
+ updatePeriodSeconds: 1
+ action: put
+ - key: spec.template.spec.containers[0].resources.limits.memory
+ value: 2G
+ action: update
+ - key: spec.template.spec.containers[0].resources.requests.memory
+ value: 1G
+ action: update
+
+ register: registryout
+
+'''
+
+# -*- -*- -*- End included fragment: doc/registry -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
+
+# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/secret.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class SecretConfig(object):
+ ''' Handle secret options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ secrets=None,
+ stype=None,
+ annotations=None):
+ ''' constructor for handling secret options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.type = stype
+ self.namespace = namespace
+ self.secrets = secrets
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a secret dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Secret'
+ self.data['type'] = self.type
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ self.data['data'] = {}
+ if self.secrets:
+ for key, value in self.secrets.items():
+ self.data['data'][key] = value
+ if self.annotations:
+ self.data['metadata']['annotations'] = self.annotations
+
+# pylint: disable=too-many-instance-attributes
+class Secret(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ secret_path = "data"
+ kind = 'secret'
+
+ def __init__(self, content):
+ '''secret constructor'''
+ super(Secret, self).__init__(content=content)
+ self._secrets = None
+
+ @property
+ def secrets(self):
+ '''secret property getter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self):
+ '''secret property setter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ def get_secrets(self):
+ ''' returns all of the defined secrets '''
+ return self.get(Secret.secret_path) or {}
+
+ def add_secret(self, key, value):
+ ''' add a secret '''
+ if self.secrets:
+ self.secrets[key] = value
+ else:
+ self.put(Secret.secret_path, {key: value})
+
+ return True
+
+ def delete_secret(self, key):
+ ''' delete secret'''
+ try:
+ del self.secrets[key]
+ except KeyError as _:
+ return False
+
+ return True
+
+ def find_secret(self, key):
+ ''' find secret'''
+ rval = None
+ try:
+ rval = self.secrets[key]
+ except KeyError as _:
+ return None
+
+ return {'key': key, 'value': rval}
+
+ def update_secret(self, key, value):
+ ''' update a secret'''
+ if key in self.secrets:
+ self.secrets[key] = value
+ else:
+ self.add_secret(key, value)
+
+ return True
+
+# -*- -*- -*- End included fragment: lib/secret.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/service.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class ServiceConfig(object):
+ ''' Handle service options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ ports,
+ selector=None,
+ labels=None,
+ cluster_ip=None,
+ portal_ip=None,
+ session_affinity=None,
+ service_type=None,
+ external_ips=None):
+ ''' constructor for handling service options '''
+ self.name = sname
+ self.namespace = namespace
+ self.ports = ports
+ self.selector = selector
+ self.labels = labels
+ self.cluster_ip = cluster_ip
+ self.portal_ip = portal_ip
+ self.session_affinity = session_affinity
+ self.service_type = service_type
+ self.external_ips = external_ips
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiates a service dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Service'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ if self.labels:
+ self.data['metadata']['labels'] = {}
+ for lab, lab_value in self.labels.items():
+ self.data['metadata']['labels'][lab] = lab_value
+ self.data['spec'] = {}
+
+ if self.ports:
+ self.data['spec']['ports'] = self.ports
+ else:
+ self.data['spec']['ports'] = []
+
+ if self.selector:
+ self.data['spec']['selector'] = self.selector
+
+ self.data['spec']['sessionAffinity'] = self.session_affinity or 'None'
+
+ if self.cluster_ip:
+ self.data['spec']['clusterIP'] = self.cluster_ip
+
+ if self.portal_ip:
+ self.data['spec']['portalIP'] = self.portal_ip
+
+ if self.service_type:
+ self.data['spec']['type'] = self.service_type
+
+ if self.external_ips:
+ self.data['spec']['externalIPs'] = self.external_ips
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class Service(Yedit):
+ ''' Class to model the oc service object '''
+ port_path = "spec.ports"
+ portal_ip = "spec.portalIP"
+ cluster_ip = "spec.clusterIP"
+ selector_path = 'spec.selector'
+ kind = 'Service'
+ external_ips = "spec.externalIPs"
+
+ def __init__(self, content):
+ '''Service constructor'''
+ super(Service, self).__init__(content=content)
+
+ def get_ports(self):
+ ''' get a list of ports '''
+ return self.get(Service.port_path) or []
+
+ def get_selector(self):
+ ''' get the service selector'''
+ return self.get(Service.selector_path) or {}
+
+ def add_ports(self, inc_ports):
+ ''' add a port object to the ports list '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get_ports()
+ if not ports:
+ self.put(Service.port_path, inc_ports)
+ else:
+ ports.extend(inc_ports)
+
+ return True
+
+ def find_ports(self, inc_port):
+ ''' find a specific port '''
+ for port in self.get_ports():
+ if port['port'] == inc_port['port']:
+ return port
+
+ return None
+
+ def delete_ports(self, inc_ports):
+ ''' remove a port from a service '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get(Service.port_path) or []
+
+ if not ports:
+ return True
+
+ removed = False
+ for inc_port in inc_ports:
+ port = self.find_ports(inc_port)
+ if port:
+ ports.remove(port)
+ removed = True
+
+ return removed
+
+ def add_cluster_ip(self, sip):
+ '''add cluster ip'''
+ self.put(Service.cluster_ip, sip)
+
+ def add_portal_ip(self, pip):
+ '''add cluster ip'''
+ self.put(Service.portal_ip, pip)
+
+ def get_external_ips(self):
+ ''' get a list of external_ips '''
+ return self.get(Service.external_ips) or []
+
+ def add_external_ips(self, inc_external_ips):
+ ''' add an external_ip to the external_ips list '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get_external_ips()
+ if not external_ips:
+ self.put(Service.external_ips, inc_external_ips)
+ else:
+ external_ips.extend(inc_external_ips)
+
+ return True
+
+ def find_external_ips(self, inc_external_ip):
+ ''' find a specific external IP '''
+ val = None
+ try:
+ idx = self.get_external_ips().index(inc_external_ip)
+ val = self.get_external_ips()[idx]
+ except ValueError:
+ pass
+
+ return val
+
+ def delete_external_ips(self, inc_external_ips):
+ ''' remove an external IP from a service '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get(Service.external_ips) or []
+
+ if not external_ips:
+ return True
+
+ removed = False
+ for inc_external_ip in inc_external_ips:
+ external_ip = self.find_external_ips(inc_external_ip)
+ if external_ip:
+ external_ips.remove(external_ip)
+ removed = True
+
+ return removed
+
+# -*- -*- -*- End included fragment: lib/service.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/volume.py -*- -*- -*-
+
+class Volume(object):
+ ''' Class to represent an openshift volume object'''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ @staticmethod
+ def create_volume_structure(volume_info):
+ ''' return a properly structured volume '''
+ volume_mount = None
+ volume = {'name': volume_info['name']}
+ volume_type = volume_info['type'].lower()
+ if volume_type == 'secret':
+ volume['secret'] = {}
+ volume[volume_info['type']] = {'secretName': volume_info['secret_name']}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'emptydir':
+ volume['emptyDir'] = {}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'pvc' or volume_type == 'persistentvolumeclaim':
+ volume['persistentVolumeClaim'] = {}
+ volume['persistentVolumeClaim']['claimName'] = volume_info['claimName']
+ volume['persistentVolumeClaim']['claimSize'] = volume_info['claimSize']
+ elif volume_type == 'hostpath':
+ volume['hostPath'] = {}
+ volume['hostPath']['path'] = volume_info['path']
+ elif volume_type == 'configmap':
+ volume['configMap'] = {}
+ volume['configMap']['name'] = volume_info['configmap_name']
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+
+ return (volume, volume_mount)
+
+# -*- -*- -*- End included fragment: lib/volume.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_version.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCVersion(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ debug):
+ ''' Constructor for OCVersion '''
+ super(OCVersion, self).__init__(None, config)
+ self.debug = debug
+
+ def get(self):
+ '''get and return version information '''
+
+ results = {}
+
+ version_results = self._version()
+
+ if version_results['returncode'] == 0:
+ filtered_vers = Utils.filter_versions(version_results['results'])
+ custom_vers = Utils.add_custom_versions(filtered_vers)
+
+ results['returncode'] = version_results['returncode']
+ results.update(filtered_vers)
+ results.update(custom_vers)
+
+ return results
+
+ raise OpenShiftCLIError('Problem detecting openshift version.')
+
+ @staticmethod
+ def run_ansible(params):
+ '''run the idempotent ansible code'''
+ oc_version = OCVersion(params['kubeconfig'], params['debug'])
+
+ if params['state'] == 'list':
+
+ #pylint: disable=protected-access
+ result = oc_version.get()
+ return {'state': params['state'],
+ 'results': result,
+ 'changed': False}
+
+# -*- -*- -*- End included fragment: class/oc_version.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_registry.py -*- -*- -*-
+
+class RegistryException(Exception):
+ ''' Registry Exception Class '''
+ pass
+
+
+class RegistryConfig(OpenShiftCLIConfig):
+ ''' RegistryConfig is a DTO for the registry. '''
+ def __init__(self, rname, namespace, kubeconfig, registry_options):
+ super(RegistryConfig, self).__init__(rname, namespace, kubeconfig, registry_options)
+
+
+class Registry(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ volume_mount_path = 'spec.template.spec.containers[0].volumeMounts'
+ volume_path = 'spec.template.spec.volumes'
+ env_path = 'spec.template.spec.containers[0].env'
+
+ def __init__(self,
+ registry_config,
+ verbose=False):
+ ''' Constructor for Registry
+
+ a registry consists of 3 or more parts
+ - dc/docker-registry
+ - svc/docker-registry
+
+ Parameters:
+ :registry_config:
+ :verbose:
+ '''
+ super(Registry, self).__init__(registry_config.namespace, registry_config.kubeconfig, verbose)
+ self.version = OCVersion(registry_config.kubeconfig, verbose)
+ self.svc_ip = None
+ self.portal_ip = None
+ self.config = registry_config
+ self.verbose = verbose
+ self.registry_parts = [{'kind': 'dc', 'name': self.config.name},
+ {'kind': 'svc', 'name': self.config.name},
+ ]
+
+ self.__prepared_registry = None
+ self.volume_mounts = []
+ self.volumes = []
+ if self.config.config_options['volume_mounts']['value']:
+ for volume in self.config.config_options['volume_mounts']['value']:
+ volume_info = {'secret_name': volume.get('secret_name', None),
+ 'name': volume.get('name', None),
+ 'type': volume.get('type', None),
+ 'path': volume.get('path', None),
+ 'claimName': volume.get('claim_name', None),
+ 'claimSize': volume.get('claim_size', None),
+ }
+
+ vol, vol_mount = Volume.create_volume_structure(volume_info)
+ self.volumes.append(vol)
+ self.volume_mounts.append(vol_mount)
+
+ self.dconfig = None
+ self.svc = None
+
+ @property
+ def deploymentconfig(self):
+ ''' deploymentconfig property '''
+ return self.dconfig
+
+ @deploymentconfig.setter
+ def deploymentconfig(self, config):
+ ''' setter for deploymentconfig property '''
+ self.dconfig = config
+
+ @property
+ def service(self):
+ ''' service property '''
+ return self.svc
+
+ @service.setter
+ def service(self, config):
+ ''' setter for service property '''
+ self.svc = config
+
+ @property
+ def prepared_registry(self):
+ ''' prepared_registry property '''
+ if not self.__prepared_registry:
+ results = self.prepare_registry()
+ if not results or ('returncode' in results and results['returncode'] != 0):
+ raise RegistryException('Could not perform registry preparation. {}'.format(results))
+ self.__prepared_registry = results
+
+ return self.__prepared_registry
+
+ @prepared_registry.setter
+ def prepared_registry(self, data):
+ ''' setter method for prepared_registry attribute '''
+ self.__prepared_registry = data
+
+ def get(self):
+ ''' return the self.registry_parts '''
+ self.deploymentconfig = None
+ self.service = None
+
+ rval = 0
+ for part in self.registry_parts:
+ result = self._get(part['kind'], name=part['name'])
+ if result['returncode'] == 0 and part['kind'] == 'dc':
+ self.deploymentconfig = DeploymentConfig(result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'svc':
+ self.service = Service(result['results'][0])
+
+ if result['returncode'] != 0:
+ rval = result['returncode']
+
+
+ return {'returncode': rval, 'deploymentconfig': self.deploymentconfig, 'service': self.service}
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.deploymentconfig and self.service:
+ return True
+
+ return False
+
+ def delete(self, complete=True):
+ '''return all pods '''
+ parts = []
+ for part in self.registry_parts:
+ if not complete and part['kind'] == 'svc':
+ continue
+ parts.append(self._delete(part['kind'], part['name']))
+
+ # Clean up returned results
+ rval = 0
+ for part in parts:
+ # pylint: disable=invalid-sequence-index
+ if 'returncode' in part and part['returncode'] != 0:
+ rval = part['returncode']
+
+ return {'returncode': rval, 'results': parts}
+
+ def prepare_registry(self):
+ ''' prepare a registry for instantiation '''
+ options = self.config.to_option_list(ascommalist='labels')
+
+ cmd = ['registry']
+ cmd.extend(options)
+ cmd.extend(['--dry-run=True', '-o', 'json'])
+
+ results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json')
+ # probably need to parse this
+ # pylint thinks results is a string
+ # pylint: disable=no-member
+ if results['returncode'] != 0 and 'items' not in results['results']:
+ raise RegistryException('Could not perform registry preparation. {}'.format(results))
+
+ service = None
+ deploymentconfig = None
+ # pylint: disable=invalid-sequence-index
+ for res in results['results']['items']:
+ if res['kind'] == 'DeploymentConfig':
+ deploymentconfig = DeploymentConfig(res)
+ elif res['kind'] == 'Service':
+ service = Service(res)
+
+ # Verify we got a service and a deploymentconfig
+ if not service or not deploymentconfig:
+ return results
+
+ # results will need to get parsed here and modifications added
+ deploymentconfig = DeploymentConfig(self.add_modifications(deploymentconfig))
+
+ # modify service ip
+ if self.svc_ip:
+ service.put('spec.clusterIP', self.svc_ip)
+ if self.portal_ip:
+ service.put('spec.portalIP', self.portal_ip)
+
+ # the dry-run doesn't apply the selector correctly
+ if self.service:
+ service.put('spec.selector', self.service.get_selector())
+
+ # need to create the service and the deploymentconfig
+ service_file = Utils.create_tmp_file_from_contents('service', service.yaml_dict)
+ deployment_file = Utils.create_tmp_file_from_contents('deploymentconfig', deploymentconfig.yaml_dict)
+
+ return {"service": service,
+ "service_file": service_file,
+ "service_update": False,
+ "deployment": deploymentconfig,
+ "deployment_file": deployment_file,
+ "deployment_update": False}
+
+ def create(self):
+ '''Create a registry'''
+ results = []
+ self.needs_update()
+ # if the object is none, then we need to create it
+ # if the object needs an update, then we should call replace
+ # Handle the deploymentconfig
+ if self.deploymentconfig is None:
+ results.append(self._create(self.prepared_registry['deployment_file']))
+ elif self.prepared_registry['deployment_update']:
+ results.append(self._replace(self.prepared_registry['deployment_file']))
+
+ # Handle the service
+ if self.service is None:
+ results.append(self._create(self.prepared_registry['service_file']))
+ elif self.prepared_registry['service_update']:
+ results.append(self._replace(self.prepared_registry['service_file']))
+
+ # Clean up returned results
+ rval = 0
+ for result in results:
+ # pylint: disable=invalid-sequence-index
+ if 'returncode' in result and result['returncode'] != 0:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ def update(self):
+ '''run update for the registry. This performs a replace if required'''
+ # Store the current service IP
+ if self.service:
+ svcip = self.service.get('spec.clusterIP')
+ if svcip:
+ self.svc_ip = svcip
+ portip = self.service.get('spec.portalIP')
+ if portip:
+ self.portal_ip = portip
+
+ results = []
+ if self.prepared_registry['deployment_update']:
+ results.append(self._replace(self.prepared_registry['deployment_file']))
+ if self.prepared_registry['service_update']:
+ results.append(self._replace(self.prepared_registry['service_file']))
+
+ # Clean up returned results
+ rval = 0
+ for result in results:
+ if result['returncode'] != 0:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ def add_modifications(self, deploymentconfig):
+ ''' update a deployment config with changes '''
+ # The environment variable for REGISTRY_HTTP_SECRET is autogenerated
+ # We should set the generated deploymentconfig to the in memory version
+ # the following modifications will overwrite if needed
+ if self.deploymentconfig:
+ result = self.deploymentconfig.get_env_var('REGISTRY_HTTP_SECRET')
+ if result:
+ deploymentconfig.update_env_var('REGISTRY_HTTP_SECRET', result['value'])
+
+ # Currently we know that our deployment of a registry requires a few extra modifications
+ # Modification 1
+ # we need specific environment variables to be set
+ for key, value in self.config.config_options['env_vars'].get('value', {}).items():
+ if not deploymentconfig.exists_env_key(key):
+ deploymentconfig.add_env_value(key, value)
+ else:
+ deploymentconfig.update_env_var(key, value)
+
+ # Modification 2
+ # we need specific volume variables to be set
+ for volume in self.volumes:
+ deploymentconfig.update_volume(volume)
+
+ for vol_mount in self.volume_mounts:
+ deploymentconfig.update_volume_mount(vol_mount)
+
+ # Modification 3
+ # Edits
+ edit_results = []
+ for edit in self.config.config_options['edits'].get('value', []):
+ if edit['action'] == 'put':
+ edit_results.append(deploymentconfig.put(edit['key'],
+ edit['value']))
+ if edit['action'] == 'update':
+ edit_results.append(deploymentconfig.update(edit['key'],
+ edit['value'],
+ edit.get('index', None),
+ edit.get('curr_value', None)))
+ if edit['action'] == 'append':
+ edit_results.append(deploymentconfig.append(edit['key'],
+ edit['value']))
+
+ if edit_results and not any([res[0] for res in edit_results]):
+ return None
+
+ return deploymentconfig.yaml_dict
+
+ def needs_update(self):
+ ''' check to see if we need to update '''
+ exclude_list = ['clusterIP', 'portalIP', 'type', 'protocol']
+ if self.service is None or \
+ not Utils.check_def_equal(self.prepared_registry['service'].yaml_dict,
+ self.service.yaml_dict,
+ exclude_list,
+ debug=self.verbose):
+ self.prepared_registry['service_update'] = True
+
+ exclude_list = ['dnsPolicy',
+ 'terminationGracePeriodSeconds',
+ 'restartPolicy', 'timeoutSeconds',
+ 'livenessProbe', 'readinessProbe',
+ 'terminationMessagePath',
+ 'securityContext',
+ 'imagePullPolicy',
+ 'protocol', # ports.portocol: TCP
+ 'type', # strategy: {'type': 'rolling'}
+ 'defaultMode', # added on secrets
+ 'activeDeadlineSeconds', # added in 1.5 for timeouts
+ ]
+
+ if self.deploymentconfig is None or \
+ not Utils.check_def_equal(self.prepared_registry['deployment'].yaml_dict,
+ self.deploymentconfig.yaml_dict,
+ exclude_list,
+ debug=self.verbose):
+ self.prepared_registry['deployment_update'] = True
+
+ return self.prepared_registry['deployment_update'] or self.prepared_registry['service_update'] or False
+
+ # In the future, we would like to break out each ansible state into a function.
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run idempotent ansible code'''
+
+ registry_options = {'images': {'value': params['images'], 'include': True},
+ 'latest_images': {'value': params['latest_images'], 'include': True},
+ 'labels': {'value': params['labels'], 'include': True},
+ 'ports': {'value': ','.join(params['ports']), 'include': True},
+ 'replicas': {'value': params['replicas'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'service_account': {'value': params['service_account'], 'include': True},
+ 'mount_host': {'value': params['mount_host'], 'include': True},
+ 'env_vars': {'value': params['env_vars'], 'include': False},
+ 'volume_mounts': {'value': params['volume_mounts'], 'include': False},
+ 'edits': {'value': params['edits'], 'include': False},
+ 'tls_key': {'value': params['tls_key'], 'include': True},
+ 'tls_certificate': {'value': params['tls_certificate'], 'include': True},
+ }
+
+ # Do not always pass the daemonset and enforce-quota parameters because they are not understood
+ # by old versions of oc.
+ # Default value is false. So, it's safe to not pass an explicit false value to oc versions which
+ # understand these parameters.
+ if params['daemonset']:
+ registry_options['daemonset'] = {'value': params['daemonset'], 'include': True}
+ if params['enforce_quota']:
+ registry_options['enforce_quota'] = {'value': params['enforce_quota'], 'include': True}
+
+ rconfig = RegistryConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ registry_options)
+
+
+ ocregistry = Registry(rconfig, params['debug'])
+
+ api_rval = ocregistry.get()
+
+ state = params['state']
+ ########
+ # get
+ ########
+ if state == 'list':
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not ocregistry.exists():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ # Unsure as to why this is angry with the return type.
+ # pylint: disable=redefined-variable-type
+ api_rval = ocregistry.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not ocregistry.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ api_rval = ocregistry.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if not params['force'] and not ocregistry.needs_update():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = ocregistry.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. %s' % state}
+
+# -*- -*- -*- End included fragment: class/oc_adm_registry.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_registry.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for registry
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ images=dict(default=None, type='str'),
+ latest_images=dict(default=False, type='bool'),
+ labels=dict(default=None, type='dict'),
+ ports=dict(default=['5000'], type='list'),
+ replicas=dict(default=1, type='int'),
+ selector=dict(default=None, type='str'),
+ service_account=dict(default='registry', type='str'),
+ mount_host=dict(default=None, type='str'),
+ volume_mounts=dict(default=None, type='list'),
+ env_vars=dict(default={}, type='dict'),
+ edits=dict(default=[], type='list'),
+ enforce_quota=dict(default=False, type='bool'),
+ force=dict(default=False, type='bool'),
+ daemonset=dict(default=False, type='bool'),
+ tls_key=dict(default=None, type='str'),
+ tls_certificate=dict(default=None, type='str'),
+ ),
+
+ supports_check_mode=True,
+ )
+
+ results = Registry.run_ansible(module.params, module.check_mode)
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_registry.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_adm_router.py b/roles/lib_openshift/library/oc_adm_router.py
new file mode 100644
index 000000000..146f71f68
--- /dev/null
+++ b/roles/lib_openshift/library/oc_adm_router.py
@@ -0,0 +1,3191 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/router -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_adm_router
+short_description: Module to manage openshift router
+description:
+ - Manage openshift router programmatically.
+options:
+ state:
+ description:
+ - Whether to create or delete the router
+ - present - create the router
+ - absent - remove the router
+ - list - return the current representation of a router
+ required: false
+ default: present
+ choices:
+ - present
+ - absent
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - The name of the router
+ required: false
+ default: router
+ aliases: []
+ namespace:
+ description:
+ - The namespace where to manage the router.
+ required: false
+ default: default
+ aliases: []
+ images:
+ description:
+ - The image to base this router on - ${component} will be replaced with --type
+ required: 'openshift3/ose-${component}:${version}'
+ default: None
+ aliases: []
+ latest_images:
+ description:
+ - If true, attempt to use the latest image for the registry instead of the latest release.
+ required: false
+ default: False
+ aliases: []
+ labels:
+ description:
+ - A set of labels to uniquely identify the registry and its components.
+ required: false
+ default: None
+ aliases: []
+ ports:
+ description:
+ - A list of strings in the 'port:port' format
+ required: False
+ default:
+ - 80:80
+ - 443:443
+ aliases: []
+ replicas:
+ description:
+ - The replication factor of the registry; commonly 2 when high availability is desired.
+ required: False
+ default: 1
+ aliases: []
+ selector:
+ description:
+ - Selector used to filter nodes on deployment. Used to run routers on a specific set of nodes.
+ required: False
+ default: None
+ aliases: []
+ service_account:
+ description:
+ - Name of the service account to use to run the router pod.
+ required: False
+ default: router
+ aliases: []
+ router_type:
+ description:
+ - The router image to use - if you specify --images this flag may be ignored.
+ required: false
+ default: haproxy-router
+ aliases: []
+ external_host:
+ description:
+ - If the underlying router implementation connects with an external host, this is the external host's hostname.
+ required: false
+ default: None
+ aliases: []
+ external_host_vserver:
+ description:
+ - If the underlying router implementation uses virtual servers, this is the name of the virtual server for HTTP connections.
+ required: false
+ default: None
+ aliases: []
+ external_host_insecure:
+ description:
+ - If the underlying router implementation connects with an external host
+ - over a secure connection, this causes the router to skip strict certificate verification with the external host.
+ required: false
+ default: False
+ aliases: []
+ external_host_partition_path:
+ description:
+ - If the underlying router implementation uses partitions for control boundaries, this is the path to use for that partition.
+ required: false
+ default: None
+ aliases: []
+ external_host_username:
+ description:
+ - If the underlying router implementation connects with an external host, this is the username for authenticating with the external host.
+ required: false
+ default: None
+ aliases: []
+ external_host_password:
+ description:
+ - If the underlying router implementation connects with an external host, this is the password for authenticating with the external host.
+ required: false
+ default: None
+ aliases: []
+ external_host_private_key:
+ description:
+ - If the underlying router implementation requires an SSH private key, this is the path to the private key file.
+ required: false
+ default: None
+ aliases: []
+ expose_metrics:
+ description:
+ - This is a hint to run an extra container in the pod to expose metrics - the image
+ - will either be set depending on the router implementation or provided with --metrics-image.
+ required: false
+ default: False
+ aliases: []
+ metrics_image:
+ description:
+ - If expose_metrics is specified this is the image to use to run a sidecar container
+ - in the pod exposing metrics. If not set and --expose-metrics is true the image will
+ - depend on router implementation.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment:
+- There are some exceptions to note when doing the idempotency in this module.
+- The strategy is to use the oc adm router command to generate a default
+- configuration when creating or updating a router. Often times there
+- differences from the generated template and what is in memory in openshift.
+- We make exceptions to not check these specific values when comparing objects.
+- Here are a list of exceptions:
+- - DeploymentConfig:
+ - dnsPolicy
+ - terminationGracePeriodSeconds
+ - restartPolicy
+ - timeoutSeconds
+ - livenessProbe
+ - readinessProbe
+ - terminationMessagePath
+ - hostPort
+ - defaultMode
+ - Service:
+ - portalIP
+ - clusterIP
+ - sessionAffinity
+ - type
+ - ServiceAccount:
+ - secrets
+ - imagePullSecrets
+'''
+
+EXAMPLES = '''
+- name: create routers
+ oc_adm_router:
+ name: router
+ service_account: router
+ replicas: 2
+ namespace: default
+ selector: type=infra
+ cert_file: /etc/origin/master/named_certificates/router.crt
+ key_file: /etc/origin/master/named_certificates/router.key
+ cacert_file: /etc/origin/master/named_certificates/router.ca
+ edits:
+ - key: spec.strategy.rollingParams
+ value:
+ intervalSeconds: 1
+ maxSurge: 50%
+ maxUnavailable: 50%
+ timeoutSeconds: 600
+ updatePeriodSeconds: 1
+ action: put
+ - key: spec.template.spec.containers[0].resources.limits.memory
+ value: 2G
+ action: put
+ - key: spec.template.spec.containers[0].resources.requests.memory
+ value: 1G
+ action: put
+ - key: spec.template.spec.containers[0].env
+ value:
+ name: EXTENDED_VALIDATION
+ value: 'false'
+ action: update
+ register: router_out
+ run_once: True
+'''
+
+# -*- -*- -*- End included fragment: doc/router -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/service.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class ServiceConfig(object):
+ ''' Handle service options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ ports,
+ selector=None,
+ labels=None,
+ cluster_ip=None,
+ portal_ip=None,
+ session_affinity=None,
+ service_type=None,
+ external_ips=None):
+ ''' constructor for handling service options '''
+ self.name = sname
+ self.namespace = namespace
+ self.ports = ports
+ self.selector = selector
+ self.labels = labels
+ self.cluster_ip = cluster_ip
+ self.portal_ip = portal_ip
+ self.session_affinity = session_affinity
+ self.service_type = service_type
+ self.external_ips = external_ips
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiates a service dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Service'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ if self.labels:
+ self.data['metadata']['labels'] = {}
+ for lab, lab_value in self.labels.items():
+ self.data['metadata']['labels'][lab] = lab_value
+ self.data['spec'] = {}
+
+ if self.ports:
+ self.data['spec']['ports'] = self.ports
+ else:
+ self.data['spec']['ports'] = []
+
+ if self.selector:
+ self.data['spec']['selector'] = self.selector
+
+ self.data['spec']['sessionAffinity'] = self.session_affinity or 'None'
+
+ if self.cluster_ip:
+ self.data['spec']['clusterIP'] = self.cluster_ip
+
+ if self.portal_ip:
+ self.data['spec']['portalIP'] = self.portal_ip
+
+ if self.service_type:
+ self.data['spec']['type'] = self.service_type
+
+ if self.external_ips:
+ self.data['spec']['externalIPs'] = self.external_ips
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class Service(Yedit):
+ ''' Class to model the oc service object '''
+ port_path = "spec.ports"
+ portal_ip = "spec.portalIP"
+ cluster_ip = "spec.clusterIP"
+ selector_path = 'spec.selector'
+ kind = 'Service'
+ external_ips = "spec.externalIPs"
+
+ def __init__(self, content):
+ '''Service constructor'''
+ super(Service, self).__init__(content=content)
+
+ def get_ports(self):
+ ''' get a list of ports '''
+ return self.get(Service.port_path) or []
+
+ def get_selector(self):
+ ''' get the service selector'''
+ return self.get(Service.selector_path) or {}
+
+ def add_ports(self, inc_ports):
+ ''' add a port object to the ports list '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get_ports()
+ if not ports:
+ self.put(Service.port_path, inc_ports)
+ else:
+ ports.extend(inc_ports)
+
+ return True
+
+ def find_ports(self, inc_port):
+ ''' find a specific port '''
+ for port in self.get_ports():
+ if port['port'] == inc_port['port']:
+ return port
+
+ return None
+
+ def delete_ports(self, inc_ports):
+ ''' remove a port from a service '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get(Service.port_path) or []
+
+ if not ports:
+ return True
+
+ removed = False
+ for inc_port in inc_ports:
+ port = self.find_ports(inc_port)
+ if port:
+ ports.remove(port)
+ removed = True
+
+ return removed
+
+ def add_cluster_ip(self, sip):
+ '''add cluster ip'''
+ self.put(Service.cluster_ip, sip)
+
+ def add_portal_ip(self, pip):
+ '''add cluster ip'''
+ self.put(Service.portal_ip, pip)
+
+ def get_external_ips(self):
+ ''' get a list of external_ips '''
+ return self.get(Service.external_ips) or []
+
+ def add_external_ips(self, inc_external_ips):
+ ''' add an external_ip to the external_ips list '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get_external_ips()
+ if not external_ips:
+ self.put(Service.external_ips, inc_external_ips)
+ else:
+ external_ips.extend(inc_external_ips)
+
+ return True
+
+ def find_external_ips(self, inc_external_ip):
+ ''' find a specific external IP '''
+ val = None
+ try:
+ idx = self.get_external_ips().index(inc_external_ip)
+ val = self.get_external_ips()[idx]
+ except ValueError:
+ pass
+
+ return val
+
+ def delete_external_ips(self, inc_external_ips):
+ ''' remove an external IP from a service '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get(Service.external_ips) or []
+
+ if not external_ips:
+ return True
+
+ removed = False
+ for inc_external_ip in inc_external_ips:
+ external_ip = self.find_external_ips(inc_external_ip)
+ if external_ip:
+ external_ips.remove(external_ip)
+ removed = True
+
+ return removed
+
+# -*- -*- -*- End included fragment: lib/service.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
+
+# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/serviceaccount.py -*- -*- -*-
+
+class ServiceAccountConfig(object):
+ '''Service account config class
+
+ This class stores the options and returns a default service account
+ '''
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None):
+ self.name = sname
+ self.kubeconfig = kubeconfig
+ self.namespace = namespace
+ self.secrets = secrets or []
+ self.image_pull_secrets = image_pull_secrets or []
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiate a properly structured volume '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'ServiceAccount'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['secrets'] = []
+ if self.secrets:
+ for sec in self.secrets:
+ self.data['secrets'].append({"name": sec})
+
+ self.data['imagePullSecrets'] = []
+ if self.image_pull_secrets:
+ for sec in self.image_pull_secrets:
+ self.data['imagePullSecrets'].append({"name": sec})
+
+class ServiceAccount(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ image_pull_secrets_path = "imagePullSecrets"
+ secrets_path = "secrets"
+
+ def __init__(self, content):
+ '''ServiceAccount constructor'''
+ super(ServiceAccount, self).__init__(content=content)
+ self._secrets = None
+ self._image_pull_secrets = None
+
+ @property
+ def image_pull_secrets(self):
+ ''' property for image_pull_secrets '''
+ if self._image_pull_secrets is None:
+ self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or []
+ return self._image_pull_secrets
+
+ @image_pull_secrets.setter
+ def image_pull_secrets(self, secrets):
+ ''' property for secrets '''
+ self._image_pull_secrets = secrets
+
+ @property
+ def secrets(self):
+ ''' property for secrets '''
+ if not self._secrets:
+ self._secrets = self.get(ServiceAccount.secrets_path) or []
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self, secrets):
+ ''' property for secrets '''
+ self._secrets = secrets
+
+ def delete_secret(self, inc_secret):
+ ''' remove a secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.secrets[remove_idx]
+ return True
+
+ return False
+
+ def delete_image_pull_secret(self, inc_secret):
+ ''' remove a image_pull_secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.image_pull_secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.image_pull_secrets[remove_idx]
+ return True
+
+ return False
+
+ def find_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def find_image_pull_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.image_pull_secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def add_secret(self, inc_secret):
+ '''add secret'''
+ if self.secrets:
+ self.secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.secrets_path, [{"name": inc_secret}])
+
+ def add_image_pull_secret(self, inc_secret):
+ '''add image_pull_secret'''
+ if self.image_pull_secrets:
+ self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}])
+
+# -*- -*- -*- End included fragment: lib/serviceaccount.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/secret.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class SecretConfig(object):
+ ''' Handle secret options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ secrets=None,
+ stype=None,
+ annotations=None):
+ ''' constructor for handling secret options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.type = stype
+ self.namespace = namespace
+ self.secrets = secrets
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a secret dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Secret'
+ self.data['type'] = self.type
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ self.data['data'] = {}
+ if self.secrets:
+ for key, value in self.secrets.items():
+ self.data['data'][key] = value
+ if self.annotations:
+ self.data['metadata']['annotations'] = self.annotations
+
+# pylint: disable=too-many-instance-attributes
+class Secret(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ secret_path = "data"
+ kind = 'secret'
+
+ def __init__(self, content):
+ '''secret constructor'''
+ super(Secret, self).__init__(content=content)
+ self._secrets = None
+
+ @property
+ def secrets(self):
+ '''secret property getter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self):
+ '''secret property setter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ def get_secrets(self):
+ ''' returns all of the defined secrets '''
+ return self.get(Secret.secret_path) or {}
+
+ def add_secret(self, key, value):
+ ''' add a secret '''
+ if self.secrets:
+ self.secrets[key] = value
+ else:
+ self.put(Secret.secret_path, {key: value})
+
+ return True
+
+ def delete_secret(self, key):
+ ''' delete secret'''
+ try:
+ del self.secrets[key]
+ except KeyError as _:
+ return False
+
+ return True
+
+ def find_secret(self, key):
+ ''' find secret'''
+ rval = None
+ try:
+ rval = self.secrets[key]
+ except KeyError as _:
+ return None
+
+ return {'key': key, 'value': rval}
+
+ def update_secret(self, key, value):
+ ''' update a secret'''
+ if key in self.secrets:
+ self.secrets[key] = value
+ else:
+ self.add_secret(key, value)
+
+ return True
+
+# -*- -*- -*- End included fragment: lib/secret.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/rolebinding.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class RoleBindingConfig(object):
+ ''' Handle rolebinding config '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kubeconfig,
+ group_names=None,
+ role_ref=None,
+ subjects=None,
+ usernames=None):
+ ''' constructor for handling rolebinding options '''
+ self.kubeconfig = kubeconfig
+ self.name = name
+ self.namespace = namespace
+ self.group_names = group_names
+ self.role_ref = role_ref
+ self.subjects = subjects
+ self.usernames = usernames
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' create a default rolebinding as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'RoleBinding'
+ self.data['groupNames'] = self.group_names
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['roleRef'] = self.role_ref
+ self.data['subjects'] = self.subjects
+ self.data['userNames'] = self.usernames
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class RoleBinding(Yedit):
+ ''' Class to model a rolebinding openshift object'''
+ group_names_path = "groupNames"
+ role_ref_path = "roleRef"
+ subjects_path = "subjects"
+ user_names_path = "userNames"
+
+ kind = 'RoleBinding'
+
+ def __init__(self, content):
+ '''RoleBinding constructor'''
+ super(RoleBinding, self).__init__(content=content)
+ self._subjects = None
+ self._role_ref = None
+ self._group_names = None
+ self._user_names = None
+
+ @property
+ def subjects(self):
+ ''' subjects property '''
+ if self._subjects is None:
+ self._subjects = self.get_subjects()
+ return self._subjects
+
+ @subjects.setter
+ def subjects(self, data):
+ ''' subjects property setter'''
+ self._subjects = data
+
+ @property
+ def role_ref(self):
+ ''' role_ref property '''
+ if self._role_ref is None:
+ self._role_ref = self.get_role_ref()
+ return self._role_ref
+
+ @role_ref.setter
+ def role_ref(self, data):
+ ''' role_ref property setter'''
+ self._role_ref = data
+
+ @property
+ def group_names(self):
+ ''' group_names property '''
+ if self._group_names is None:
+ self._group_names = self.get_group_names()
+ return self._group_names
+
+ @group_names.setter
+ def group_names(self, data):
+ ''' group_names property setter'''
+ self._group_names = data
+
+ @property
+ def user_names(self):
+ ''' user_names property '''
+ if self._user_names is None:
+ self._user_names = self.get_user_names()
+ return self._user_names
+
+ @user_names.setter
+ def user_names(self, data):
+ ''' user_names property setter'''
+ self._user_names = data
+
+ def get_group_names(self):
+ ''' return groupNames '''
+ return self.get(RoleBinding.group_names_path) or []
+
+ def get_user_names(self):
+ ''' return usernames '''
+ return self.get(RoleBinding.user_names_path) or []
+
+ def get_role_ref(self):
+ ''' return role_ref '''
+ return self.get(RoleBinding.role_ref_path) or {}
+
+ def get_subjects(self):
+ ''' return subjects '''
+ return self.get(RoleBinding.subjects_path) or []
+
+ #### ADD #####
+ def add_subject(self, inc_subject):
+ ''' add a subject '''
+ if self.subjects:
+ # pylint: disable=no-member
+ self.subjects.append(inc_subject)
+ else:
+ self.put(RoleBinding.subjects_path, [inc_subject])
+
+ return True
+
+ def add_role_ref(self, inc_role_ref):
+ ''' add a role_ref '''
+ if not self.role_ref:
+ self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
+ return True
+
+ return False
+
+ def add_group_names(self, inc_group_names):
+ ''' add a group_names '''
+ if self.group_names:
+ # pylint: disable=no-member
+ self.group_names.append(inc_group_names)
+ else:
+ self.put(RoleBinding.group_names_path, [inc_group_names])
+
+ return True
+
+ def add_user_name(self, inc_user_name):
+ ''' add a username '''
+ if self.user_names:
+ # pylint: disable=no-member
+ self.user_names.append(inc_user_name)
+ else:
+ self.put(RoleBinding.user_names_path, [inc_user_name])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_subject(self, inc_subject):
+ ''' remove a subject '''
+ try:
+ # pylint: disable=no-member
+ self.subjects.remove(inc_subject)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_role_ref(self, inc_role_ref):
+ ''' remove a role_ref '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref:
+ del self.role_ref['name']
+ return True
+
+ return False
+
+ def remove_group_name(self, inc_group_name):
+ ''' remove a groupname '''
+ try:
+ # pylint: disable=no-member
+ self.group_names.remove(inc_group_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_user_name(self, inc_user_name):
+ ''' remove a username '''
+ try:
+ # pylint: disable=no-member
+ self.user_names.remove(inc_user_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_subject(self, inc_subject):
+ ''' update a subject '''
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return self.add_subject(inc_subject)
+
+ self.subjects[index] = inc_subject
+
+ return True
+
+ def update_group_name(self, inc_group_name):
+ ''' update a groupname '''
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return self.add_group_names(inc_group_name)
+
+ self.group_names[index] = inc_group_name
+
+ return True
+
+ def update_user_name(self, inc_user_name):
+ ''' update a username '''
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return self.add_user_name(inc_user_name)
+
+ self.user_names[index] = inc_user_name
+
+ return True
+
+ def update_role_ref(self, inc_role_ref):
+ ''' update a role_ref '''
+ self.role_ref['name'] = inc_role_ref
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_subject(self, inc_subject):
+ ''' find a subject '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group_name(self, inc_group_name):
+ ''' find a group_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_user_name(self, inc_user_name):
+ ''' find a user_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_role_ref(self, inc_role_ref):
+ ''' find a user_name '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
+ return self.role_ref
+
+ return None
+
+# -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_adm_router.py -*- -*- -*-
+
+
+class RouterException(Exception):
+ ''' Router exception'''
+ pass
+
+
+class RouterConfig(OpenShiftCLIConfig):
+ ''' RouterConfig is a DTO for the router. '''
+ def __init__(self, rname, namespace, kubeconfig, router_options):
+ super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options)
+
+
+class Router(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ def __init__(self,
+ router_config,
+ verbose=False):
+ ''' Constructor for OpenshiftOC
+
+ a router consists of 3 or more parts
+ - dc/router
+ - svc/router
+ - sa/router
+ - secret/router-certs
+ - clusterrolebinding/router-router-role
+ '''
+ super(Router, self).__init__('default', router_config.kubeconfig, verbose)
+ self.config = router_config
+ self.verbose = verbose
+ self.router_parts = [{'kind': 'dc', 'name': self.config.name},
+ {'kind': 'svc', 'name': self.config.name},
+ {'kind': 'sa', 'name': self.config.config_options['service_account']['value']},
+ {'kind': 'secret', 'name': self.config.name + '-certs'},
+ {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'},
+ ]
+
+ self.__prepared_router = None
+ self.dconfig = None
+ self.svc = None
+ self._secret = None
+ self._serviceaccount = None
+ self._rolebinding = None
+
+ @property
+ def prepared_router(self):
+ ''' property for the prepared router'''
+ if self.__prepared_router is None:
+ results = self._prepare_router()
+ if not results or 'returncode' in results and results['returncode'] != 0:
+ if 'stderr' in results:
+ raise RouterException('Could not perform router preparation: %s' % results['stderr'])
+
+ raise RouterException('Could not perform router preparation.')
+ self.__prepared_router = results
+
+ return self.__prepared_router
+
+ @prepared_router.setter
+ def prepared_router(self, obj):
+ '''setter for the prepared_router'''
+ self.__prepared_router = obj
+
+ @property
+ def deploymentconfig(self):
+ ''' property deploymentconfig'''
+ return self.dconfig
+
+ @deploymentconfig.setter
+ def deploymentconfig(self, config):
+ ''' setter for property deploymentconfig '''
+ self.dconfig = config
+
+ @property
+ def service(self):
+ ''' property for service '''
+ return self.svc
+
+ @service.setter
+ def service(self, config):
+ ''' setter for property service '''
+ self.svc = config
+
+ @property
+ def secret(self):
+ ''' property secret '''
+ return self._secret
+
+ @secret.setter
+ def secret(self, config):
+ ''' setter for property secret '''
+ self._secret = config
+
+ @property
+ def serviceaccount(self):
+ ''' property for serviceaccount '''
+ return self._serviceaccount
+
+ @serviceaccount.setter
+ def serviceaccount(self, config):
+ ''' setter for property serviceaccount '''
+ self._serviceaccount = config
+
+ @property
+ def rolebinding(self):
+ ''' property rolebinding '''
+ return self._rolebinding
+
+ @rolebinding.setter
+ def rolebinding(self, config):
+ ''' setter for property rolebinding '''
+ self._rolebinding = config
+
+ def get_object_by_kind(self, kind):
+ '''return the current object kind by name'''
+ if re.match("^(dc|deploymentconfig)$", kind, flags=re.IGNORECASE):
+ return self.deploymentconfig
+ elif re.match("^(svc|service)$", kind, flags=re.IGNORECASE):
+ return self.service
+ elif re.match("^(sa|serviceaccount)$", kind, flags=re.IGNORECASE):
+ return self.serviceaccount
+ elif re.match("secret", kind, flags=re.IGNORECASE):
+ return self.secret
+ elif re.match("clusterrolebinding", kind, flags=re.IGNORECASE):
+ return self.rolebinding
+
+ return None
+
+ def get(self):
+ ''' return the self.router_parts '''
+ self.service = None
+ self.deploymentconfig = None
+ self.serviceaccount = None
+ self.secret = None
+ self.rolebinding = None
+ for part in self.router_parts:
+ result = self._get(part['kind'], name=part['name'])
+ if result['returncode'] == 0 and part['kind'] == 'dc':
+ self.deploymentconfig = DeploymentConfig(result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'svc':
+ self.service = Service(content=result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'sa':
+ self.serviceaccount = ServiceAccount(content=result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'secret':
+ self.secret = Secret(content=result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding':
+ self.rolebinding = RoleBinding(content=result['results'][0])
+
+ return {'deploymentconfig': self.deploymentconfig,
+ 'service': self.service,
+ 'serviceaccount': self.serviceaccount,
+ 'secret': self.secret,
+ 'clusterrolebinding': self.rolebinding,
+ }
+
+ def exists(self):
+ '''return a whether svc or dc exists '''
+ if self.deploymentconfig and self.service and self.secret and self.serviceaccount:
+ return True
+
+ return False
+
+ def delete(self):
+ '''return all pods '''
+ parts = []
+ for part in self.router_parts:
+ parts.append(self._delete(part['kind'], part['name']))
+
+ rval = 0
+ for part in parts:
+ if part['returncode'] != 0 and not 'already exist' in part['stderr']:
+ rval = part['returncode']
+
+ return {'returncode': rval, 'results': parts}
+
+ def add_modifications(self, deploymentconfig):
+ '''modify the deployment config'''
+ # We want modifications in the form of edits coming in from the module.
+ # Let's apply these here
+ edit_results = []
+ for edit in self.config.config_options['edits'].get('value', []):
+ if edit['action'] == 'put':
+ edit_results.append(deploymentconfig.put(edit['key'],
+ edit['value']))
+ if edit['action'] == 'update':
+ edit_results.append(deploymentconfig.update(edit['key'],
+ edit['value'],
+ edit.get('index', None),
+ edit.get('curr_value', None)))
+ if edit['action'] == 'append':
+ edit_results.append(deploymentconfig.append(edit['key'],
+ edit['value']))
+
+ if edit_results and not any([res[0] for res in edit_results]):
+ return None
+
+ return deploymentconfig
+
+ # pylint: disable=too-many-branches
+ def _prepare_router(self):
+ '''prepare router for instantiation'''
+ # if cacert, key, and cert were passed, combine them into a pem file
+ if (self.config.config_options['cacert_file']['value'] and
+ self.config.config_options['cert_file']['value'] and
+ self.config.config_options['key_file']['value']):
+
+ router_pem = '/tmp/router.pem'
+ with open(router_pem, 'w') as rfd:
+ rfd.write(open(self.config.config_options['cert_file']['value']).read())
+ rfd.write(open(self.config.config_options['key_file']['value']).read())
+ if self.config.config_options['cacert_file']['value'] and \
+ os.path.exists(self.config.config_options['cacert_file']['value']):
+ rfd.write(open(self.config.config_options['cacert_file']['value']).read())
+
+ atexit.register(Utils.cleanup, [router_pem])
+
+ self.config.config_options['default_cert']['value'] = router_pem
+
+ elif self.config.config_options['default_cert']['value'] is None:
+ # No certificate was passed to us. do not pass one to oc adm router
+ self.config.config_options['default_cert']['include'] = False
+
+ options = self.config.to_option_list(ascommalist='labels')
+
+ cmd = ['router', self.config.name]
+ cmd.extend(options)
+ cmd.extend(['--dry-run=True', '-o', 'json'])
+
+ results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json')
+
+ # pylint: disable=maybe-no-member
+ if results['returncode'] != 0 or 'items' not in results['results']:
+ return results
+
+ oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False},
+ 'Secret': {'obj': None, 'path': None, 'update': False},
+ 'ServiceAccount': {'obj': None, 'path': None, 'update': False},
+ 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False},
+ 'Service': {'obj': None, 'path': None, 'update': False},
+ }
+ # pylint: disable=invalid-sequence-index
+ for res in results['results']['items']:
+ if res['kind'] == 'DeploymentConfig':
+ oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res)
+ elif res['kind'] == 'Service':
+ oc_objects['Service']['obj'] = Service(res)
+ elif res['kind'] == 'ServiceAccount':
+ oc_objects['ServiceAccount']['obj'] = ServiceAccount(res)
+ elif res['kind'] == 'Secret':
+ oc_objects['Secret']['obj'] = Secret(res)
+ elif res['kind'] == 'ClusterRoleBinding':
+ oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res)
+
+ # Currently only deploymentconfig needs updating
+ # Verify we got a deploymentconfig
+ if not oc_objects['DeploymentConfig']['obj']:
+ return results
+
+ # add modifications added
+ oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj'])
+
+ for oc_type, oc_data in oc_objects.items():
+ if oc_data['obj'] is not None:
+ oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict)
+
+ return oc_objects
+
+ def create(self):
+ '''Create a router
+
+ This includes the different parts:
+ - deploymentconfig
+ - service
+ - serviceaccount
+ - secrets
+ - clusterrolebinding
+ '''
+ results = []
+ self.needs_update()
+
+ import time
+ # pylint: disable=maybe-no-member
+ for kind, oc_data in self.prepared_router.items():
+ if oc_data['obj'] is not None:
+ time.sleep(1)
+ if self.get_object_by_kind(kind) is None:
+ results.append(self._create(oc_data['path']))
+
+ elif oc_data['update']:
+ results.append(self._replace(oc_data['path']))
+
+
+ rval = 0
+ for result in results:
+ if result['returncode'] != 0 and not 'already exist' in result['stderr']:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ def update(self):
+ '''run update for the router. This performs a replace'''
+ results = []
+
+ # pylint: disable=maybe-no-member
+ for _, oc_data in self.prepared_router.items():
+ if oc_data['update']:
+ results.append(self._replace(oc_data['path']))
+
+ rval = 0
+ for result in results:
+ if result['returncode'] != 0:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ def needs_update(self):
+ ''' check to see if we need to update '''
+ # ServiceAccount:
+ # Need to determine changes from the pregenerated ones from the original
+ # Since these are auto generated, we can skip
+ skip = ['secrets', 'imagePullSecrets']
+ if self.serviceaccount is None or \
+ not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict,
+ self.serviceaccount.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['ServiceAccount']['update'] = True
+
+ # Secret:
+ # See if one was generated from our dry-run and verify it if needed
+ if self.prepared_router['Secret']['obj']:
+ if not self.secret:
+ self.prepared_router['Secret']['update'] = True
+
+ if self.secret is None or \
+ not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict,
+ self.secret.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['Secret']['update'] = True
+
+ # Service:
+ # Fix the ports to have protocol=TCP
+ for port in self.prepared_router['Service']['obj'].get('spec.ports'):
+ port['protocol'] = 'TCP'
+
+ skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type']
+ if self.service is None or \
+ not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict,
+ self.service.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['Service']['update'] = True
+
+ # DeploymentConfig:
+ # Router needs some exceptions.
+ # We do not want to check the autogenerated password for stats admin
+ if self.deploymentconfig is not None:
+ if not self.config.config_options['stats_password']['value']:
+ for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
+ 'spec.template.spec.containers[0].env') or []):
+ if env_var['name'] == 'STATS_PASSWORD':
+ env_var['value'] = \
+ self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx)
+ break
+
+ # dry-run doesn't add the protocol to the ports section. We will manually do that.
+ for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
+ 'spec.template.spec.containers[0].ports') or []):
+ if not 'protocol' in port:
+ port['protocol'] = 'TCP'
+
+ # These are different when generating
+ skip = ['dnsPolicy',
+ 'terminationGracePeriodSeconds',
+ 'restartPolicy', 'timeoutSeconds',
+ 'livenessProbe', 'readinessProbe',
+ 'terminationMessagePath', 'hostPort',
+ 'defaultMode',
+ ]
+
+ if self.deploymentconfig is None or \
+ not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict,
+ self.deploymentconfig.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['DeploymentConfig']['update'] = True
+
+ # Check if any of the parts need updating, if so, return True
+ # else, no need to update
+ # pylint: disable=no-member
+ return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()])
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run ansible idempotent code'''
+
+ rconfig = RouterConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ {'default_cert': {'value': params['default_cert'], 'include': True},
+ 'cert_file': {'value': params['cert_file'], 'include': False},
+ 'key_file': {'value': params['key_file'], 'include': False},
+ 'images': {'value': params['images'], 'include': True},
+ 'latest_images': {'value': params['latest_images'], 'include': True},
+ 'labels': {'value': params['labels'], 'include': True},
+ 'ports': {'value': ','.join(params['ports']), 'include': True},
+ 'replicas': {'value': params['replicas'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'service_account': {'value': params['service_account'], 'include': True},
+ 'router_type': {'value': params['router_type'], 'include': False},
+ 'host_network': {'value': params['host_network'], 'include': True},
+ 'external_host': {'value': params['external_host'], 'include': True},
+ 'external_host_vserver': {'value': params['external_host_vserver'],
+ 'include': True},
+ 'external_host_insecure': {'value': params['external_host_insecure'],
+ 'include': True},
+ 'external_host_partition_path': {'value': params['external_host_partition_path'],
+ 'include': True},
+ 'external_host_username': {'value': params['external_host_username'],
+ 'include': True},
+ 'external_host_password': {'value': params['external_host_password'],
+ 'include': True},
+ 'external_host_private_key': {'value': params['external_host_private_key'],
+ 'include': True},
+ 'expose_metrics': {'value': params['expose_metrics'], 'include': True},
+ 'metrics_image': {'value': params['metrics_image'], 'include': True},
+ 'stats_user': {'value': params['stats_user'], 'include': True},
+ 'stats_password': {'value': params['stats_password'], 'include': True},
+ 'stats_port': {'value': params['stats_port'], 'include': True},
+ # extra
+ 'cacert_file': {'value': params['cacert_file'], 'include': False},
+ # edits
+ 'edits': {'value': params['edits'], 'include': False},
+ })
+
+
+ state = params['state']
+
+ ocrouter = Router(rconfig, verbose=params['debug'])
+
+ api_rval = ocrouter.get()
+
+ ########
+ # get
+ ########
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not ocrouter.exists():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ # In case of delete we return a list of each object
+ # that represents a router and its result in a list
+ # pylint: disable=redefined-variable-type
+ api_rval = ocrouter.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not ocrouter.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ api_rval = ocrouter.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if not ocrouter.needs_update():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = ocrouter.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+# -*- -*- -*- End included fragment: class/oc_adm_router.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_adm_router.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for router
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default='router', type='str'),
+
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ default_cert=dict(default=None, type='str'),
+ cert_file=dict(default=None, type='str'),
+ key_file=dict(default=None, type='str'),
+ images=dict(default=None, type='str'), #'openshift3/ose-${component}:${version}'
+ latest_images=dict(default=False, type='bool'),
+ labels=dict(default=None, type='dict'),
+ ports=dict(default=['80:80', '443:443'], type='list'),
+ replicas=dict(default=1, type='int'),
+ selector=dict(default=None, type='str'),
+ service_account=dict(default='router', type='str'),
+ router_type=dict(default='haproxy-router', type='str'),
+ host_network=dict(default=True, type='bool'),
+ # external host options
+ external_host=dict(default=None, type='str'),
+ external_host_vserver=dict(default=None, type='str'),
+ external_host_insecure=dict(default=False, type='bool'),
+ external_host_partition_path=dict(default=None, type='str'),
+ external_host_username=dict(default=None, type='str'),
+ external_host_password=dict(default=None, type='str'),
+ external_host_private_key=dict(default=None, type='str'),
+ # Metrics
+ expose_metrics=dict(default=False, type='bool'),
+ metrics_image=dict(default=None, type='str'),
+ # Stats
+ stats_user=dict(default=None, type='str'),
+ stats_password=dict(default=None, type='str'),
+ stats_port=dict(default=1936, type='int'),
+ # extra
+ cacert_file=dict(default=None, type='str'),
+ # edits
+ edits=dict(default=[], type='list'),
+ ),
+ mutually_exclusive=[["router_type", "images"],
+ ["key_file", "default_cert"],
+ ["cert_file", "default_cert"],
+ ["cacert_file", "default_cert"],
+ ],
+
+ required_together=[['cacert_file', 'cert_file', 'key_file']],
+ supports_check_mode=True,
+ )
+ results = Router.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_adm_router.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_atomic_container.py b/roles/lib_openshift/library/oc_atomic_container.py
new file mode 100644
index 000000000..79bd08f4e
--- /dev/null
+++ b/roles/lib_openshift/library/oc_atomic_container.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: doc/atomic_container -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_atomic_container
+short_description: Manage the container images on the atomic host platform
+description:
+ - Manage the container images on the atomic host platform
+ - Allows to execute the commands on the container images
+requirements:
+ - atomic
+ - "python >= 2.6"
+options:
+ name:
+ description:
+ - Name of the container
+ required: True
+ default: null
+ image:
+ description:
+ - The image to use to install the container
+ required: True
+ default: null
+ state:
+ description:
+ - State of the container
+ required: True
+ choices: ["latest", "absent", "latest", "rollback"]
+ default: "latest"
+ values:
+ description:
+ - Values for the installation of the container
+ required: False
+ default: None
+'''
+
+# -*- -*- -*- End included fragment: doc/atomic_container -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_atomic_container.py -*- -*- -*-
+
+# pylint: disable=wrong-import-position,too-many-branches,invalid-name,no-name-in-module, import-error
+import json
+
+from distutils.version import StrictVersion
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def _install(module, container, image, values_list):
+ ''' install a container using atomic CLI. values_list is the list of --set arguments.
+ container is the name given to the container. image is the image to use for the installation. '''
+ # NOTE: system-package=no is hardcoded. This should be changed to an option in the future.
+ args = ['atomic', 'install', '--system', '--system-package=no',
+ '--name=%s' % container] + values_list + [image]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ return rc, out, err, False
+ else:
+ changed = "Extracting" in out or "Copying blob" in out
+ return rc, out, err, changed
+
+def _uninstall(module, name):
+ ''' uninstall an atomic container by its name. '''
+ args = ['atomic', 'uninstall', name]
+ rc, out, err = module.run_command(args, check_rc=False)
+ return rc, out, err, False
+
+
+def do_install(module, container, image, values_list):
+ ''' install a container and exit the module. '''
+ rc, out, err, changed = _install(module, container, image, values_list)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ else:
+ module.exit_json(msg=out, changed=changed)
+
+
+def do_uninstall(module, name):
+ ''' uninstall a container and exit the module. '''
+ rc, out, err, changed = _uninstall(module, name)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ module.exit_json(msg=out, changed=changed)
+
+
+def do_update(module, container, old_image, image, values_list):
+ ''' update a container and exit the module. If the container uses a different
+ image than the current installed one, then first uninstall the old one '''
+
+ # the image we want is different than the installed one
+ if old_image != image:
+ rc, out, err, _ = _uninstall(module, container)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ return do_install(module, container, image, values_list)
+
+ # if the image didn't change, use "atomic containers update"
+ args = ['atomic', 'containers', 'update'] + values_list + [container]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ else:
+ changed = "Extracting" in out or "Copying blob" in out
+ module.exit_json(msg=out, changed=changed)
+
+
+def do_rollback(module, name):
+ ''' move to the previous deployment of the container, if present, and exit the module. '''
+ args = ['atomic', 'containers', 'rollback', name]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ else:
+ changed = "Rolling back" in out
+ module.exit_json(msg=out, changed=changed)
+
+
+def core(module):
+ ''' entrypoint for the module. '''
+ name = module.params['name']
+ image = module.params['image']
+ values = module.params['values']
+ state = module.params['state']
+
+ module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
+ out = {}
+ err = {}
+ rc = 0
+
+ values_list = ["--set=%s" % x for x in values] if values else []
+
+ args = ['atomic', 'containers', 'list', '--json', '--all', '-f', 'container=%s' % name]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ return
+
+ # NOTE: "or '[]' is a workaround until atomic containers list --json
+ # provides an empty list when no containers are present.
+ containers = json.loads(out or '[]')
+ present = len(containers) > 0
+ old_image = containers[0]["image_name"] if present else None
+
+ if state == 'present' and present:
+ module.exit_json(msg=out, changed=False)
+ elif (state in ['latest', 'present']) and not present:
+ do_install(module, name, image, values_list)
+ elif state == 'latest':
+ do_update(module, name, old_image, image, values_list)
+ elif state == 'absent':
+ if not present:
+ module.exit_json(msg="", changed=False)
+ else:
+ do_uninstall(module, name)
+ elif state == 'rollback':
+ do_rollback(module, name)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ name=dict(default=None, required=True),
+ image=dict(default=None, required=True),
+ state=dict(default='latest', choices=['present', 'absent', 'latest', 'rollback']),
+ values=dict(type='list', default=[]),
+ ),
+ )
+
+ # Verify that the platform supports atomic command
+ rc, version_out, err = module.run_command('rpm -q --queryformat "%{VERSION}\n" atomic', check_rc=False)
+ if rc != 0:
+ module.fail_json(msg="Error in running atomic command", err=err)
+ # This module requires atomic version 1.17.2 or later
+ atomic_version = StrictVersion(version_out.replace('\n', ''))
+ if atomic_version < StrictVersion('1.17.2'):
+ module.fail_json(
+ msg="atomic version 1.17.2+ is required",
+ err=str(atomic_version))
+
+ try:
+ core(module)
+ except Exception as e: # pylint: disable=broad-except
+ module.fail_json(msg=str(e))
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_atomic_container.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_clusterrole.py b/roles/lib_openshift/library/oc_clusterrole.py
new file mode 100644
index 000000000..9761b4b4e
--- /dev/null
+++ b/roles/lib_openshift/library/oc_clusterrole.py
@@ -0,0 +1,1814 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/clusterrole -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_clusterrole
+short_description: Modify, and idempotently manage openshift clusterroles
+description:
+ - Manage openshift clusterroles
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a clusterrole
+ - absent - will remove a clusterrole
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ rules:
+ description:
+ - A list of dictionaries that have the rule parameters.
+ - e.g. rules=[{'apiGroups': [""], 'attributeRestrictions': None, 'verbs': ['get'], 'resources': []}]
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: query a list of env vars on dc
+ oc_clusterrole:
+ name: myclusterrole
+ state: list
+
+- name: Set the following variables.
+ oc_clusterrole:
+ name: myclusterrole
+ rules:
+ apiGroups:
+ - ""
+ attributeRestrictions: null
+ verbs: []
+ resources: []
+'''
+
+# -*- -*- -*- End included fragment: doc/clusterrole -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/rule.py -*- -*- -*-
+
+
+class Rule(object):
+ '''class to represent a clusterrole rule
+
+ Example Rule Object's yaml:
+ - apiGroups:
+ - ""
+ attributeRestrictions: null
+ resources:
+ - persistentvolumes
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+
+ '''
+ def __init__(self,
+ api_groups=None,
+ attr_restrictions=None,
+ resources=None,
+ verbs=None):
+ self.__api_groups = api_groups if api_groups is not None else [""]
+ self.__verbs = verbs if verbs is not None else []
+ self.__resources = resources if resources is not None else []
+ self.__attribute_restrictions = attr_restrictions if attr_restrictions is not None else None
+
+ @property
+ def verbs(self):
+ '''property for verbs'''
+ if self.__verbs is None:
+ return []
+
+ return self.__verbs
+
+ @verbs.setter
+ def verbs(self, data):
+ '''setter for verbs'''
+ self.__verbs = data
+
+ @property
+ def api_groups(self):
+ '''property for api_groups'''
+ if self.__api_groups is None:
+ return []
+ return self.__api_groups
+
+ @api_groups.setter
+ def api_groups(self, data):
+ '''setter for api_groups'''
+ self.__api_groups = data
+
+ @property
+ def resources(self):
+ '''property for resources'''
+ if self.__resources is None:
+ return []
+
+ return self.__resources
+
+ @resources.setter
+ def resources(self, data):
+ '''setter for resources'''
+ self.__resources = data
+
+ @property
+ def attribute_restrictions(self):
+ '''property for attribute_restrictions'''
+ return self.__attribute_restrictions
+
+ @attribute_restrictions.setter
+ def attribute_restrictions(self, data):
+ '''setter for attribute_restrictions'''
+ self.__attribute_restrictions = data
+
+ def add_verb(self, inc_verb):
+ '''add a verb to the verbs array'''
+ self.verbs.append(inc_verb)
+
+ def add_api_group(self, inc_apigroup):
+ '''add an api_group to the api_groups array'''
+ self.api_groups.append(inc_apigroup)
+
+ def add_resource(self, inc_resource):
+ '''add an resource to the resources array'''
+ self.resources.append(inc_resource)
+
+ def remove_verb(self, inc_verb):
+ '''add a verb to the verbs array'''
+ try:
+ self.verbs.remove(inc_verb)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def remove_api_group(self, inc_api_group):
+ '''add a verb to the verbs array'''
+ try:
+ self.api_groups.remove(inc_api_group)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def remove_resource(self, inc_resource):
+ '''add a verb to the verbs array'''
+ try:
+ self.resources.remove(inc_resource)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def __eq__(self, other):
+ '''return whether rules are equal'''
+ return (self.attribute_restrictions == other.attribute_restrictions and
+ self.api_groups == other.api_groups and
+ self.resources == other.resources and
+ self.verbs == other.verbs)
+
+
+ @staticmethod
+ def parse_rules(inc_rules):
+ '''create rules from an array'''
+
+ results = []
+ for rule in inc_rules:
+ results.append(Rule(rule.get('apiGroups', ['']),
+ rule.get('attributeRestrictions', None),
+ rule.get('resources', []),
+ rule.get('verbs', [])))
+
+ return results
+
+# -*- -*- -*- End included fragment: lib/rule.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/clusterrole.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class ClusterRole(Yedit):
+ ''' Class to model an openshift ClusterRole'''
+ rules_path = "rules"
+
+ def __init__(self, name=None, content=None):
+ ''' Constructor for clusterrole '''
+ if content is None:
+ content = ClusterRole.builder(name).yaml_dict
+
+ super(ClusterRole, self).__init__(content=content)
+
+ self.__rules = Rule.parse_rules(self.get(ClusterRole.rules_path)) or []
+
+ @property
+ def rules(self):
+ return self.__rules
+
+ @rules.setter
+ def rules(self, data):
+ self.__rules = data
+ self.put(ClusterRole.rules_path, self.__rules)
+
+ def rule_exists(self, inc_rule):
+ '''attempt to find the inc_rule in the rules list'''
+ for rule in self.rules:
+ if rule == inc_rule:
+ return True
+
+ return False
+
+ def compare(self, other, verbose=False):
+ '''compare function for clusterrole'''
+ for rule in other.rules:
+ if rule not in self.rules:
+ if verbose:
+ print('Rule in other not found in self. [{}]'.format(rule))
+ return False
+
+ for rule in self.rules:
+ if rule not in other.rules:
+ if verbose:
+ print('Rule in self not found in other. [{}]'.format(rule))
+ return False
+
+ return True
+
+ @staticmethod
+ def builder(name='default_clusterrole', rules=None):
+ '''return a clusterrole with name and/or rules'''
+ if rules is None:
+ rules = [{'apiGroups': [""],
+ 'attributeRestrictions': None,
+ 'verbs': [],
+ 'resources': []}]
+ content = {
+ 'apiVersion': 'v1',
+ 'kind': 'ClusterRole',
+ 'metadata': {'name': '{}'.format(name)},
+ 'rules': rules,
+ }
+
+ return ClusterRole(content=content)
+
+
+# -*- -*- -*- End included fragment: lib/clusterrole.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_clusterrole.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCClusterRole(OpenShiftCLI):
+ ''' Class to manage clusterrole objects'''
+ kind = 'clusterrole'
+
+ def __init__(self,
+ name,
+ rules=None,
+ kubeconfig=None,
+ verbose=False):
+ ''' Constructor for OCClusterRole '''
+ super(OCClusterRole, self).__init__(None, kubeconfig=kubeconfig, verbose=verbose)
+ self.verbose = verbose
+ self.name = name
+ self._clusterrole = None
+ self._inc_clusterrole = ClusterRole.builder(name, rules)
+
+ @property
+ def clusterrole(self):
+ ''' property for clusterrole'''
+ if self._clusterrole is None:
+ self.get()
+ return self._clusterrole
+
+ @clusterrole.setter
+ def clusterrole(self, data):
+ ''' setter function for clusterrole property'''
+ self._clusterrole = data
+
+ @property
+ def inc_clusterrole(self):
+ ''' property for inc_clusterrole'''
+ return self._inc_clusterrole
+
+ @inc_clusterrole.setter
+ def inc_clusterrole(self, data):
+ ''' setter function for inc_clusterrole property'''
+ self._inc_clusterrole = data
+
+ def exists(self):
+ ''' return whether a clusterrole exists '''
+ if self.clusterrole:
+ return True
+
+ return False
+
+ def get(self):
+ '''return a clusterrole '''
+ result = self._get(self.kind, self.name)
+
+ if result['returncode'] == 0:
+ self.clusterrole = ClusterRole(content=result['results'][0])
+ result['results'] = self.clusterrole.yaml_dict
+
+ elif '"{}" not found'.format(self.name) in result['stderr']:
+ result['returncode'] = 0
+ self.clusterrole = None
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.name)
+
+ def create(self):
+ '''create a clusterrole from the proposed incoming clusterrole'''
+ return self._create_from_content(self.name, self.inc_clusterrole.yaml_dict)
+
+ def update(self):
+ '''update a project'''
+ return self._replace_content(self.kind, self.name, self.inc_clusterrole.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed'''
+ return not self.clusterrole.compare(self.inc_clusterrole, self.verbose)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ oc_clusterrole = OCClusterRole(params['name'],
+ params['rules'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_clusterrole.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_clusterrole.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_clusterrole.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_clusterrole.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_clusterrole.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_clusterrole.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_clusterrole.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = oc_clusterrole.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_clusterrole.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. [%s]' % state}
+
+# -*- -*- -*- End included fragment: class/oc_clusterrole.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_clusterrole.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for clusterrole
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, type='str'),
+ rules=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = OCClusterRole.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_clusterrole.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_configmap.py b/roles/lib_openshift/library/oc_configmap.py
new file mode 100644
index 000000000..047edffbb
--- /dev/null
+++ b/roles/lib_openshift/library/oc_configmap.py
@@ -0,0 +1,1631 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/configmap -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_configmap
+short_description: Modify, and idempotently manage openshift configmaps
+description:
+ - Modify openshift configmaps programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a configmap
+ - absent - will remove the configmap
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: True
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ from_file:
+ description:
+ - A dict of key, value pairs representing the configmap key and the value represents the file path.
+ required: false
+ default: None
+ aliases: []
+ from_literal:
+ description:
+ - A dict of key, value pairs representing the configmap key and the value represents the string content
+ required: false
+ default: None
+ aliases: []
+author:
+- "kenny woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create group
+ oc_configmap:
+ state: present
+ name: testmap
+ from_file:
+ secret: /path/to/secret
+ from_literal:
+ title: systemadmin
+ register: configout
+'''
+
+# -*- -*- -*- End included fragment: doc/configmap -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_configmap.py -*- -*- -*-
+
+
+# pylint: disable=too-many-arguments
+class OCConfigMap(OpenShiftCLI):
+ ''' Openshift ConfigMap Class
+
+ ConfigMaps are a way to store data inside of objects
+ '''
+ def __init__(self,
+ name,
+ from_file,
+ from_literal,
+ state,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCConfigMap, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = name
+ self.state = state
+ self._configmap = None
+ self._inc_configmap = None
+ self.from_file = from_file if from_file is not None else {}
+ self.from_literal = from_literal if from_literal is not None else {}
+
+ @property
+ def configmap(self):
+ if self._configmap is None:
+ self._configmap = self.get()
+
+ return self._configmap
+
+ @configmap.setter
+ def configmap(self, inc_map):
+ self._configmap = inc_map
+
+ @property
+ def inc_configmap(self):
+ if self._inc_configmap is None:
+ results = self.create(dryrun=True, output=True)
+ self._inc_configmap = results['results']
+
+ return self._inc_configmap
+
+ @inc_configmap.setter
+ def inc_configmap(self, inc_map):
+ self._inc_configmap = inc_map
+
+ def from_file_to_params(self):
+ '''return from_files in a string ready for cli'''
+ return ["--from-file={}={}".format(key, value) for key, value in self.from_file.items()]
+
+ def from_literal_to_params(self):
+ '''return from_literal in a string ready for cli'''
+ return ["--from-literal={}={}".format(key, value) for key, value in self.from_literal.items()]
+
+ def get(self):
+ '''return a configmap by name '''
+ results = self._get('configmap', self.name)
+ if results['returncode'] == 0 and results['results'][0]:
+ self.configmap = results['results'][0]
+
+ if results['returncode'] != 0 and '"{}" not found'.format(self.name) in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete a configmap by name'''
+ return self._delete('configmap', self.name)
+
+ def create(self, dryrun=False, output=False):
+ '''Create a configmap
+
+ :dryrun: Product what you would have done. default: False
+ :output: Whether to parse output. default: False
+ '''
+
+ cmd = ['create', 'configmap', self.name]
+ if self.from_literal is not None:
+ cmd.extend(self.from_literal_to_params())
+
+ if self.from_file is not None:
+ cmd.extend(self.from_file_to_params())
+
+ if dryrun:
+ cmd.extend(['--dry-run', '-ojson'])
+
+ results = self.openshift_cmd(cmd, output=output)
+
+ return results
+
+ def update(self):
+ '''run update configmap '''
+ return self._replace_content('configmap', self.name, self.inc_configmap)
+
+ def needs_update(self):
+ '''compare the current configmap with the proposed and return if they are equal'''
+ return not Utils.check_def_equal(self.inc_configmap, self.configmap, debug=self.verbose)
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ oc_cm = OCConfigMap(params['name'],
+ params['from_file'],
+ params['from_literal'],
+ params['state'],
+ params['namespace'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_cm.get()
+
+ if 'failed' in api_rval:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if not params['name']:
+ return {'failed': True,
+ 'msg': 'Please specify a name when state is absent|present.'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_cm.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Create
+ ########
+ if state == 'present':
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ api_rval = oc_cm.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ api_rval = oc_cm.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_cm.needs_update():
+
+ api_rval = oc_cm.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ api_rval = oc_cm.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_configmap.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_configmap.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for managing OpenShift configmap objects
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ from_file=dict(default=None, type='dict'),
+ from_literal=dict(default=None, type='dict'),
+ ),
+ supports_check_mode=True,
+ )
+
+
+ rval = OCConfigMap.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_configmap.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_edit.py b/roles/lib_openshift/library/oc_edit.py
new file mode 100644
index 000000000..0b6a8436b
--- /dev/null
+++ b/roles/lib_openshift/library/oc_edit.py
@@ -0,0 +1,1591 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/edit -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_edit
+short_description: Modify, and idempotently manage openshift objects.
+description:
+ - Modify openshift objects programmatically.
+options:
+ state:
+ description:
+ - Currently present is only supported state.
+ required: true
+ default: present
+ choices: ["present"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ kind:
+ description:
+ - The kind attribute of the object.
+ required: True
+ default: None
+ choices:
+ - bc
+ - buildconfig
+ - configmaps
+ - dc
+ - deploymentconfig
+ - imagestream
+ - imagestreamtag
+ - is
+ - istag
+ - namespace
+ - project
+ - projects
+ - node
+ - ns
+ - persistentvolume
+ - pv
+ - rc
+ - replicationcontroller
+ - routes
+ - scc
+ - secret
+ - securitycontextconstraints
+ - service
+ - svc
+ aliases: []
+ file_name:
+ description:
+ - The file name in which to edit
+ required: false
+ default: None
+ aliases: []
+ file_format:
+ description:
+ - The format of the file being edited.
+ required: false
+ default: yaml
+ aliases: []
+ content:
+ description:
+ - Content of the file
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to force the operation
+ required: false
+ default: None
+ aliases: []
+ separator:
+ description:
+ - The separator format for the edit.
+ required: false
+ default: '.'
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_edit:
+ kind: rc
+ name: hawkular-cassandra-rc
+ namespace: openshift-infra
+ content:
+ spec.template.spec.containers[0].resources.limits.memory: 512
+ spec.template.spec.containers[0].resources.requests.memory: 256
+'''
+
+# -*- -*- -*- End included fragment: doc/edit -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_edit.py -*- -*- -*-
+
+class Edit(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools
+ '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ namespace,
+ resource_name=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ separator='.',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(Edit, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.kind = kind
+ self.name = resource_name
+ self.separator = separator
+
+ def get(self):
+ '''return a secret by name '''
+ return self._get(self.kind, self.name)
+
+ def update(self, file_name, content, force=False, content_type='yaml'):
+ '''run update '''
+ if file_name:
+ if content_type == 'yaml':
+ data = yaml.load(open(file_name))
+ elif content_type == 'json':
+ data = json.loads(open(file_name).read())
+
+ changes = []
+ yed = Yedit(filename=file_name, content=data, separator=self.separator)
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([not change[0] for change in changes]):
+ return {'returncode': 0, 'updated': False}
+
+ yed.write()
+
+ atexit.register(Utils.cleanup, [file_name])
+
+ return self._replace(file_name, force=force)
+
+ return self._replace_content(self.kind, self.name, content, force=force, sep=self.separator)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ ocedit = Edit(params['kind'],
+ params['namespace'],
+ params['name'],
+ kubeconfig=params['kubeconfig'],
+ separator=params['separator'],
+ verbose=params['debug'])
+
+ api_rval = ocedit.get()
+
+ ########
+ # Create
+ ########
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {"failed": True, 'msg': api_rval}
+
+ ########
+ # Update
+ ########
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed edit'}
+
+ api_rval = ocedit.update(params['file_name'],
+ params['content'],
+ params['force'],
+ params['file_format'])
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, 'msg': api_rval}
+
+ if 'updated' in api_rval and not api_rval['updated']:
+ return {"changed": False, 'results': api_rval, 'state': 'present'}
+
+ # return the created object
+ api_rval = ocedit.get()
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, 'msg': api_rval}
+
+ return {"changed": True, 'results': api_rval, 'state': 'present'}
+
+# -*- -*- -*- End included fragment: class/oc_edit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_edit.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for editing objects
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ kind=dict(required=True,
+ type='str',
+ choices=['dc', 'deploymentconfig',
+ 'rc', 'replicationcontroller',
+ 'svc', 'service',
+ 'scc', 'securitycontextconstraints',
+ 'ns', 'namespace', 'project', 'projects',
+ 'is', 'imagestream',
+ 'istag', 'imagestreamtag',
+ 'bc', 'buildconfig',
+ 'routes',
+ 'node',
+ 'secret',
+ 'pv', 'persistentvolume']),
+ file_name=dict(default=None, type='str'),
+ file_format=dict(default='yaml', type='str'),
+ content=dict(default=None, required=True, type='dict'),
+ force=dict(default=False, type='bool'),
+ separator=dict(default='.', type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = Edit.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_edit.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_env.py b/roles/lib_openshift/library/oc_env.py
new file mode 100644
index 000000000..1f52fba40
--- /dev/null
+++ b/roles/lib_openshift/library/oc_env.py
@@ -0,0 +1,1946 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/env -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_env
+short_description: Modify, and idempotently manage openshift environment variables on pods, deploymentconfigs, and replication controllers.
+description:
+ - Modify openshift environment variables programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a list of environment variables
+ - absent - will remove the environment varibale from the object
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ kind:
+ description:
+ - The kind attribute of the object.
+ required: False
+ default: dc
+ choices:
+ - rc
+ - dc
+ - pods
+ aliases: []
+ env_vars:
+ description:
+ - The environment variables to insert. The format is a dict of value pairs.
+ - e.g. {key1: value1, key2: value2})
+ required: False
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: query a list of env vars on dc
+ oc_env:
+ kind: dc
+ name: myawesomedc
+ namespace: phpapp
+
+- name: Set the following variables.
+ oc_env:
+ kind: dc
+ name: myawesomedc
+ namespace: phpapp
+ env_vars:
+ SUPER_TURBO_MODE: 'true'
+ ENABLE_PORTS: 'false'
+ SERVICE_PORT: 9999
+'''
+
+# -*- -*- -*- End included fragment: doc/env -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
+
+# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_env.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCEnv(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ container_path = {"pod": "spec.containers[0].env",
+ "dc": "spec.template.spec.containers[0].env",
+ "rc": "spec.template.spec.containers[0].env",
+ }
+
+ # pylint allows 5. we need 6
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ namespace,
+ kind,
+ env_vars,
+ resource_name=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCEnv, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.kind = kind
+ self.name = resource_name
+ self.env_vars = env_vars
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var'''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var'''
+ self._resource = data
+
+ def key_value_exists(self, key, value):
+ ''' return whether a key, value pair exists '''
+ return self.resource.exists_env_value(key, value)
+
+ def key_exists(self, key):
+ ''' return whether a key exists '''
+ return self.resource.exists_env_key(key)
+
+ def get(self):
+ '''return environment variables '''
+ result = self._get(self.kind, self.name)
+ if result['returncode'] == 0:
+ if self.kind == 'dc':
+ self.resource = DeploymentConfig(content=result['results'][0])
+ result['results'] = self.resource.get(OCEnv.container_path[self.kind]) or []
+ return result
+
+ def delete(self):
+ ''' delete environment variables '''
+ if self.resource.delete_env_var(self.env_vars.keys()):
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ return {'returncode': 0, 'changed': False}
+
+ def put(self):
+ '''place env vars into dc '''
+ for update_key, update_value in self.env_vars.items():
+ self.resource.update_env_var(update_key, update_value)
+
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ ocenv = OCEnv(params['namespace'],
+ params['kind'],
+ params['env_vars'],
+ resource_name=params['name'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocenv.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ for key in params.get('env_vars', {}).keys():
+ if ocenv.resource.exists_env_key(key):
+
+ if check_mode:
+ return {'changed': False,
+ 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = ocenv.delete()
+
+ return {'changed': True, 'state': 'absent'}
+
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ for key, value in params.get('env_vars', {}).items():
+ if not ocenv.key_value_exists(key, value):
+
+ if check_mode:
+ return {'changed': False,
+ 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = ocenv.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = ocenv.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval['results'], 'state': 'present'}
+
+ return {'changed': False, 'results': api_rval['results'], 'state': 'present'}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state}
+
+# -*- -*- -*- End included fragment: class/oc_env.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_env.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for environment variables
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='rc', choices=['dc', 'rc', 'pods'], type='str'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ env_vars=dict(default=None, type='dict'),
+ ),
+ mutually_exclusive=[["content", "files"]],
+
+ supports_check_mode=True,
+ )
+ results = OCEnv.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_env.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_group.py b/roles/lib_openshift/library/oc_group.py
new file mode 100644
index 000000000..1b63a6c13
--- /dev/null
+++ b/roles/lib_openshift/library/oc_group.py
@@ -0,0 +1,1610 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/group -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_group
+short_description: Modify, and idempotently manage openshift groups.
+description:
+ - Modify openshift groups programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a group
+ - absent - will remove the group
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create group
+ oc_group:
+ state: present
+ name: acme_org
+ register: group_out
+'''
+
+# -*- -*- -*- End included fragment: doc/group -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/group.py -*- -*- -*-
+
+
+class GroupConfig(object):
+ ''' Handle route options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig):
+ ''' constructor for handling group options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Group'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['users'] = None
+
+
+# pylint: disable=too-many-instance-attributes
+class Group(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'group'
+
+ def __init__(self, content):
+ '''Group constructor'''
+ super(Group, self).__init__(content=content)
+
+# -*- -*- -*- End included fragment: lib/group.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_group.py -*- -*- -*-
+
+
+class OCGroup(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'group'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCGroup '''
+ super(OCGroup, self).__init__(config.namespace, config.kubeconfig)
+ self.config = config
+ self.namespace = config.namespace
+ self._group = None
+
+ @property
+ def group(self):
+ ''' property function service'''
+ if not self._group:
+ self.get()
+ return self._group
+
+ @group.setter
+ def group(self, data):
+ ''' setter function for yedit var '''
+ self._group = data
+
+ def exists(self):
+ ''' return whether a group exists '''
+ if self.group:
+ return True
+
+ return False
+
+ def get(self):
+ '''return group information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.group = Group(content=result['results'][0])
+ elif 'groups \"{}\" not found'.format(self.config.name) in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ return not Utils.check_def_equal(self.config.data, self.group.yaml_dict, skip_keys=[], debug=True)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+
+ gconfig = GroupConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ )
+ oc_group = OCGroup(gconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_group.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_group.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_group.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_group.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_group.needs_update():
+ api_rval = oc_group.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_group.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_group.py -*- -*- -*-
+
+#pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for group
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, type='str'),
+ namespace=dict(default='default', type='str'),
+ # addind users to a group is handled through the oc_users module
+ #users=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCGroup.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_group.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_image.py b/roles/lib_openshift/library/oc_image.py
new file mode 100644
index 000000000..94b08d9ce
--- /dev/null
+++ b/roles/lib_openshift/library/oc_image.py
@@ -0,0 +1,1536 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/image -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_image
+short_description: Create, modify, and idempotently manage openshift labels.
+description:
+ - Modify openshift labels programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create. Does _not_ support update.
+ - 'list' will read the labels
+ default: present
+ choices: ["present", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace where this object lives
+ required: false
+ default: default
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ registry_url:
+ description:
+ - The url for the registry so that openshift can pull the image
+ required: false
+ default: None
+ aliases: []
+ image_name:
+ description:
+ - The name of the image being imported
+ required: false
+ default: False
+ aliases: []
+ image_tag:
+ description:
+ - The tag of the image being imported
+ required: false
+ default: None
+ aliases: []
+author:
+- "Ivan Horvath<ihorvath@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Get an imagestream
+ oc_image:
+ name: php55
+ state: list
+ register: imageout
+
+- name: create an imagestream
+ oc_image:
+ state: present
+ image_name: php55
+ image_tag: int
+ registry_url: registry.example.com
+ namespace: default
+ register: imageout
+'''
+
+# -*- -*- -*- End included fragment: doc/image -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_image.py -*- -*- -*-
+
+
+# pylint: disable=too-many-arguments
+class OCImage(OpenShiftCLI):
+ ''' Class to import and create an imagestream object'''
+ def __init__(self,
+ namespace,
+ registry_url,
+ image_name,
+ image_tag,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCImage'''
+ super(OCImage, self).__init__(namespace, kubeconfig)
+ self.registry_url = registry_url
+ self.image_name = image_name
+ self.image_tag = image_tag
+ self.verbose = verbose
+
+ def get(self):
+ '''return a image by name '''
+ results = self._get('imagestream', self.image_name)
+ results['exists'] = False
+ if results['returncode'] == 0 and results['results'][0]:
+ results['exists'] = True
+
+ if results['returncode'] != 0 and '"{}" not found'.format(self.image_name) in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def create(self, url=None, name=None, tag=None):
+ '''Create an image '''
+ return self._import_image(url, name, tag)
+
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ ''' run the ansible idempotent code '''
+
+ ocimage = OCImage(params['namespace'],
+ params['registry_url'],
+ params['image_name'],
+ params['image_tag'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocimage.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+ return {"changed": False, "results": api_rval, "state": "list"}
+
+ ########
+ # Create
+ ########
+ if state == 'present':
+
+ if not Utils.exists(api_rval['results'], params['image_name']):
+
+ if check_mode:
+ return {"changed": False, "msg": 'CHECK_MODE: Would have performed a create'}
+
+ api_rval = ocimage.create(params['registry_url'],
+ params['image_name'],
+ params['image_tag'])
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ # return the newly created object
+ api_rval = ocimage.get()
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ return {"changed": True, "results": api_rval, "state": "present"}
+
+ # image exists, no change
+ return {"changed": False, "results": api_rval, "state": "present"}
+
+ return {"failed": True, "changed": False, "msg": "Unknown state passed. {0}".format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_image.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_image.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for image import
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ registry_url=dict(default=None, type='str'),
+ image_name=dict(default=None, required=True, type='str'),
+ image_tag=dict(default=None, type='str'),
+ force=dict(default=False, type='bool'),
+ ),
+
+ supports_check_mode=True,
+ )
+
+ rval = OCImage.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_image.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_label.py b/roles/lib_openshift/library/oc_label.py
new file mode 100644
index 000000000..ad837fdb5
--- /dev/null
+++ b/roles/lib_openshift/library/oc_label.py
@@ -0,0 +1,1752 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/label -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_label
+short_description: Create, modify, and idempotently manage openshift labels.
+description:
+ - Modify openshift labels programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update and object to the desired state
+ - 'absent' will ensure certain labels are removed
+ - 'list' will read the labels
+ - 'add' will insert labels to the already existing labels
+ default: present
+ choices: ["present", "absent", "list", "add"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ kind:
+ description:
+ - The kind of object that can be managed.
+ default: node
+ choices:
+ - node
+ - pod
+ - namespace
+ aliases: []
+ labels:
+ description:
+ - A list of labels for the resource.
+ - Each list consists of a key and a value.
+ - eg, {'key': 'foo', 'value': 'bar'}
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - The selector to apply to the resource query
+ required: false
+ default: None
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Add a single label to a node's existing labels
+ oc_label:
+ name: ip-172-31-5-23.ec2.internal
+ state: add
+ kind: node
+ labels:
+ - key: logging-infra-fluentd
+ value: 'true'
+
+- name: remove a label from a node
+ oc_label:
+ name: ip-172-31-5-23.ec2.internal
+ state: absent
+ kind: node
+ labels:
+ - key: color
+ value: blue
+
+- name: Ensure node has these exact labels
+ oc_label:
+ name: ip-172-31-5-23.ec2.internal
+ state: present
+ kind: node
+ labels:
+ - key: color
+ value: green
+ - key: type
+ value: master
+ - key: environment
+ value: production
+'''
+
+# -*- -*- -*- End included fragment: doc/label -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_label.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCLabel(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kind,
+ kubeconfig,
+ labels=None,
+ selector=None,
+ verbose=False):
+ ''' Constructor for OCLabel '''
+ super(OCLabel, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = name
+ self.kind = kind
+ self.labels = labels
+ self._curr_labels = None
+ self.selector = selector
+
+ @property
+ def current_labels(self):
+ '''property for the current labels'''
+ if self._curr_labels is None:
+ results = self.get()
+ self._curr_labels = results['labels']
+
+ return self._curr_labels
+
+ @current_labels.setter
+ def current_labels(self, data):
+ '''property setter for current labels'''
+ self._curr_labels = data
+
+ def compare_labels(self, host_labels):
+ ''' compare incoming labels against current labels'''
+
+ for label in self.labels:
+ if label['key'] not in host_labels or \
+ label['value'] != host_labels[label['key']]:
+ return False
+ return True
+
+ def all_user_labels_exist(self):
+ ''' return whether all the labels already exist '''
+
+ for current_host_labels in self.current_labels:
+ rbool = self.compare_labels(current_host_labels)
+ if not rbool:
+ return False
+ return True
+
+ def any_label_exists(self):
+ ''' return whether any single label already exists '''
+
+ for current_host_labels in self.current_labels:
+ for label in self.labels:
+ if label['key'] in current_host_labels:
+ return True
+ return False
+
+ def get_user_keys(self):
+ ''' go through list of user key:values and return all keys '''
+
+ user_keys = []
+ for label in self.labels:
+ user_keys.append(label['key'])
+
+ return user_keys
+
+ def get_current_label_keys(self):
+ ''' collect all the current label keys '''
+
+ current_label_keys = []
+ for current_host_labels in self.current_labels:
+ for key in current_host_labels.keys():
+ current_label_keys.append(key)
+
+ return list(set(current_label_keys))
+
+ def get_extra_current_labels(self):
+ ''' return list of labels that are currently stored, but aren't
+ in user-provided list '''
+
+ extra_labels = []
+ user_label_keys = self.get_user_keys()
+ current_label_keys = self.get_current_label_keys()
+
+ for current_key in current_label_keys:
+ if current_key not in user_label_keys:
+ extra_labels.append(current_key)
+
+ return extra_labels
+
+ def extra_current_labels(self):
+ ''' return whether there are labels currently stored that user
+ hasn't directly provided '''
+ extra_labels = self.get_extra_current_labels()
+
+ if len(extra_labels) > 0:
+ return True
+
+ return False
+
+ def replace(self):
+ ''' replace currently stored labels with user provided labels '''
+ cmd = self.cmd_template()
+
+ # First delete any extra labels
+ extra_labels = self.get_extra_current_labels()
+ if len(extra_labels) > 0:
+ for label in extra_labels:
+ cmd.append("{}-".format(label))
+
+ # Now add/modify the user-provided label list
+ if len(self.labels) > 0:
+ for label in self.labels:
+ cmd.append("{}={}".format(label['key'], label['value']))
+
+ # --overwrite for the case where we are updating existing labels
+ cmd.append("--overwrite")
+ return self.openshift_cmd(cmd)
+
+ def get(self):
+ '''return label information '''
+
+ result_dict = {}
+ label_list = []
+
+ if self.name:
+ result = self._get(resource=self.kind, name=self.name, selector=self.selector)
+
+ if result['results'][0] and 'labels' in result['results'][0]['metadata']:
+ label_list.append(result['results'][0]['metadata']['labels'])
+ else:
+ label_list.append({})
+
+ else:
+ result = self._get(resource=self.kind, selector=self.selector)
+
+ for item in result['results'][0]['items']:
+ if 'labels' in item['metadata']:
+ label_list.append(item['metadata']['labels'])
+ else:
+ label_list.append({})
+
+ self.current_labels = label_list
+ result_dict['labels'] = self.current_labels
+ result_dict['item_count'] = len(self.current_labels)
+ result['results'] = result_dict
+
+ return result
+
+ def cmd_template(self):
+ ''' boilerplate oc command for modifying lables on this object '''
+ # let's build the cmd with what we have passed in
+ cmd = ["label", self.kind]
+
+ if self.selector:
+ cmd.extend(["--selector", self.selector])
+ elif self.name:
+ cmd.extend([self.name])
+
+ return cmd
+
+ def add(self):
+ ''' add labels '''
+ cmd = self.cmd_template()
+
+ for label in self.labels:
+ cmd.append("{}={}".format(label['key'], label['value']))
+
+ cmd.append("--overwrite")
+
+ return self.openshift_cmd(cmd)
+
+ def delete(self):
+ '''delete the labels'''
+ cmd = self.cmd_template()
+ for label in self.labels:
+ cmd.append("{}-".format(label['key']))
+
+ return self.openshift_cmd(cmd)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent ansible code
+
+ prams comes from the ansible portion of this module
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+ oc_label = OCLabel(params['name'],
+ params['namespace'],
+ params['kind'],
+ params['kubeconfig'],
+ params['labels'],
+ params['selector'],
+ verbose=params['debug'])
+
+ state = params['state']
+ name = params['name']
+ selector = params['selector']
+
+ api_rval = oc_label.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ #######
+ # Add
+ #######
+ if state == 'add':
+ if not (name or selector):
+ return {'failed': True,
+ 'msg': "Param 'name' or 'selector' is required if state == 'add'"}
+ if not oc_label.all_user_labels_exist():
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed an addition.'}
+ api_rval = oc_label.add()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "add"}
+
+ return {'changed': False, 'state': "add"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not (name or selector):
+ return {'failed': True,
+ 'msg': "Param 'name' or 'selector' is required if state == 'absent'"}
+
+ if oc_label.any_label_exists():
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_label.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Update
+ ########
+ if not (name or selector):
+ return {'failed': True,
+ 'msg': "Param 'name' or 'selector' is required if state == 'present'"}
+ # if all the labels passed in don't already exist
+ # or if there are currently stored labels that haven't
+ # been passed in
+ if not oc_label.all_user_labels_exist() or \
+ oc_label.extra_current_labels():
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have made changes.'}
+
+ api_rval = oc_label.replace()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_label.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+ return {'failed': True,
+ 'changed': False,
+ 'results': 'Unknown state passed. %s' % state,
+ 'state': "unknown"}
+
+# -*- -*- -*- End included fragment: class/oc_label.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_label.py -*- -*- -*-
+
+def main():
+ ''' ansible oc module for labels '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list', 'add']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='node', type='str',
+ choices=['node', 'pod', 'namespace']),
+ name=dict(default=None, type='str'),
+ namespace=dict(default=None, type='str'),
+ labels=dict(default=None, type='list'),
+ selector=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ mutually_exclusive=(['name', 'selector']),
+ )
+
+ results = OCLabel.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_label.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_obj.py b/roles/lib_openshift/library/oc_obj.py
new file mode 100644
index 000000000..892546e56
--- /dev/null
+++ b/roles/lib_openshift/library/oc_obj.py
@@ -0,0 +1,1678 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/obj -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_obj
+short_description: Generic interface to openshift objects
+description:
+ - Manage openshift objects programmatically.
+options:
+ state:
+ description:
+ - Currently present is only supported state.
+ required: true
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ all_namespaces:
+ description:
+ - Search in all namespaces for the object.
+ required: false
+ default: false
+ aliases: []
+ kind:
+ description:
+ - The kind attribute of the object. e.g. dc, bc, svc, route. May be a comma-separated list, e.g. "dc,po,svc".
+ required: True
+ default: None
+ aliases: []
+ files:
+ description:
+ - A list of files provided for object
+ required: false
+ default: None
+ aliases: []
+ delete_after:
+ description:
+ - Whether or not to delete the files after processing them.
+ required: false
+ default: false
+ aliases: []
+ content:
+ description:
+ - Content of the object being managed.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to force the operation
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - Selector that gets added to the query.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_obj:
+ kind: dc
+ name: router
+ namespace: default
+register: router_output
+'''
+
+# -*- -*- -*- End included fragment: doc/obj -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_obj.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCObject(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5. we need 6
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ namespace,
+ name=None,
+ selector=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCObject, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose,
+ all_namespaces=all_namespaces)
+ self.kind = kind
+ self.name = name
+ self.selector = selector
+
+ def get(self):
+ '''return a kind by name '''
+ results = self._get(self.kind, name=self.name, selector=self.selector)
+ if (results['returncode'] != 0 and 'stderr' in results and
+ '\"{}\" not found'.format(self.name) in results['stderr']):
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete the object'''
+ results = self._delete(self.kind, name=self.name, selector=self.selector)
+ if (results['returncode'] != 0 and 'stderr' in results and
+ '\"{}\" not found'.format(self.name) in results['stderr']):
+ results['returncode'] = 0
+
+ return results
+
+ def create(self, files=None, content=None):
+ '''
+ Create a config
+
+ NOTE: This creates the first file OR the first conent.
+ TODO: Handle all files and content passed in
+ '''
+ if files:
+ return self._create(files[0])
+
+ # pylint: disable=no-member
+ # The purpose of this change is twofold:
+ # - we need a check to only use the ruamel specific dumper if ruamel is loaded
+ # - the dumper or the flow style change is needed so openshift is able to parse
+ # the resulting yaml, at least until gopkg.in/yaml.v2 is updated
+ if hasattr(yaml, 'RoundTripDumper'):
+ content['data'] = yaml.dump(content['data'], Dumper=yaml.RoundTripDumper)
+ else:
+ content['data'] = yaml.safe_dump(content['data'], default_flow_style=False)
+
+ content_file = Utils.create_tmp_files_from_contents(content)[0]
+
+ return self._create(content_file['path'])
+
+ # pylint: disable=too-many-function-args
+ def update(self, files=None, content=None, force=False):
+ '''update a current openshift object
+
+ This receives a list of file names or content
+ and takes the first and calls replace.
+
+ TODO: take an entire list
+ '''
+ if files:
+ return self._replace(files[0], force)
+
+ if content and 'data' in content:
+ content = content['data']
+
+ return self.update_content(content, force)
+
+ def update_content(self, content, force=False):
+ '''update an object through using the content param'''
+ return self._replace_content(self.kind, self.name, content, force=force)
+
+ def needs_update(self, files=None, content=None, content_type='yaml'):
+ ''' check to see if we need to update '''
+ objects = self.get()
+ if objects['returncode'] != 0:
+ return objects
+
+ data = None
+ if files:
+ data = Utils.get_resource_file(files[0], content_type)
+ elif content and 'data' in content:
+ data = content['data']
+ else:
+ data = content
+
+ # if equal then no need. So not equal is True
+ return not Utils.check_def_equal(data, objects['results'][0], skip_keys=None, debug=False)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''perform the ansible idempotent code'''
+
+ ocobj = OCObject(params['kind'],
+ params['namespace'],
+ params['name'],
+ params['selector'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'],
+ all_namespaces=params['all_namespaces'])
+
+ state = params['state']
+
+ api_rval = ocobj.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ # verify its not in our results
+ if (params['name'] is not None or params['selector'] is not None) and \
+ (len(api_rval['results']) == 0 or \
+ ('items' in api_rval['results'][0] and len(api_rval['results'][0]['items']) == 0)):
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete'}
+
+ api_rval = ocobj.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ # create/update: Must define a name beyond this point
+ if not params['name']:
+ return {'failed': True, 'msg': 'Please specify a name when state is present.'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create'}
+
+ # Create it here
+ api_rval = ocobj.create(params['files'], params['content'])
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = ocobj.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # Remove files
+ if params['files'] and params['delete_after']:
+ Utils.cleanup(params['files'])
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ # if a file path is passed, use it.
+ update = ocobj.needs_update(params['files'], params['content'])
+ if not isinstance(update, bool):
+ return {'failed': True, 'msg': update}
+
+ # No changes
+ if not update:
+ if params['files'] and params['delete_after']:
+ Utils.cleanup(params['files'])
+
+ return {'changed': False, 'results': api_rval['results'][0], 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = ocobj.update(params['files'],
+ params['content'],
+ params['force'])
+
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = ocobj.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+# -*- -*- -*- End included fragment: class/oc_obj.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_obj.py -*- -*- -*-
+
+# pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for services
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ all_namespaces=dict(defaul=False, type='bool'),
+ name=dict(default=None, type='str'),
+ files=dict(default=None, type='list'),
+ kind=dict(required=True, type='str'),
+ delete_after=dict(default=False, type='bool'),
+ content=dict(default=None, type='dict'),
+ force=dict(default=False, type='bool'),
+ selector=dict(default=None, type='str'),
+ ),
+ mutually_exclusive=[["content", "files"], ["selector", "name"]],
+
+ supports_check_mode=True,
+ )
+ rval = OCObject.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_obj.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_objectvalidator.py b/roles/lib_openshift/library/oc_objectvalidator.py
new file mode 100644
index 000000000..38df585f0
--- /dev/null
+++ b/roles/lib_openshift/library/oc_objectvalidator.py
@@ -0,0 +1,1475 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/objectvalidator -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_objectvalidator
+short_description: Validate OpenShift objects
+description:
+ - Validate OpenShift objects
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+author:
+- "Mo Khan <monis@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_objectvalidator:
+- name: run oc_objectvalidator
+ oc_objectvalidator:
+ register: oc_objectvalidator
+'''
+
+# -*- -*- -*- End included fragment: doc/objectvalidator -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_objectvalidator.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCObjectValidator(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ def __init__(self, kubeconfig):
+ ''' Constructor for OCObjectValidator '''
+ # namespace has no meaning for object validation, hardcode to 'default'
+ super(OCObjectValidator, self).__init__('default', kubeconfig)
+
+ def get_invalid(self, kind, invalid_filter):
+ ''' return invalid object information '''
+
+ rval = self._get(kind)
+ if rval['returncode'] != 0:
+ return False, rval, []
+
+ return True, rval, list(filter(invalid_filter, rval['results'][0]['items'])) # wrap filter with list for py3
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params):
+ ''' run the idempotent ansible code
+
+ params comes from the ansible portion of this module
+ '''
+
+ objectvalidator = OCObjectValidator(params['kubeconfig'])
+ all_invalid = {}
+ failed = False
+
+ def _is_invalid_namespace(namespace):
+ # check if it uses a reserved name
+ name = namespace['metadata']['name']
+ if not any((name == 'kube',
+ name == 'kubernetes',
+ name == 'openshift',
+ name.startswith('kube-'),
+ name.startswith('kubernetes-'),
+ name.startswith('openshift-'),)):
+ return False
+
+ # determine if the namespace was created by a user
+ if 'annotations' not in namespace['metadata']:
+ return False
+ return 'openshift.io/requester' in namespace['metadata']['annotations']
+
+ checks = (
+ (
+ 'hostsubnet',
+ lambda x: x['metadata']['name'] != x['host'],
+ u'hostsubnets where metadata.name != host',
+ ),
+ (
+ 'netnamespace',
+ lambda x: x['metadata']['name'] != x['netname'],
+ u'netnamespaces where metadata.name != netname',
+ ),
+ (
+ 'namespace',
+ _is_invalid_namespace,
+ u'namespaces that use reserved names and were not created by infrastructure components',
+ ),
+ )
+
+ for resource, invalid_filter, invalid_msg in checks:
+ success, rval, invalid = objectvalidator.get_invalid(resource, invalid_filter)
+ if not success:
+ return {'failed': True, 'msg': 'Failed to GET {}.'.format(resource), 'state': 'list', 'results': rval}
+ if invalid:
+ failed = True
+ all_invalid[invalid_msg] = invalid
+
+ if failed:
+ return {
+ 'failed': True,
+ 'msg': (
+ "All objects are not valid. If you are a supported customer please contact "
+ "Red Hat Support providing the complete output above. If you are not a customer "
+ "please contact users@lists.openshift.redhat.com for assistance."
+ ),
+ 'state': 'list',
+ 'results': all_invalid
+ }
+
+ return {'msg': 'All objects are valid.'}
+
+# -*- -*- -*- End included fragment: class/oc_objectvalidator.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_objectvalidator.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for validating OpenShift objects
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ ),
+ supports_check_mode=False,
+ )
+
+
+ rval = OCObjectValidator.run_ansible(module.params)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_objectvalidator.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_process.py b/roles/lib_openshift/library/oc_process.py
new file mode 100644
index 000000000..70632f86d
--- /dev/null
+++ b/roles/lib_openshift/library/oc_process.py
@@ -0,0 +1,1637 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/process -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_process
+short_description: Module to process openshift templates
+description:
+ - Process openshift templates programmatically.
+options:
+ state:
+ description:
+ - State has a few different meanings when it comes to process.
+ - state: present - This state runs an `oc process <template>`. When used in
+ - conjunction with 'create: True' the process will be piped to | oc create -f
+ - state: absent - will remove a template
+ - state: list - will perform an `oc get template <template_name>`
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ template_name:
+ description:
+ - Name of the openshift template that is being processed.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the template lives.
+ required: false
+ default: default
+ aliases: []
+ content:
+ description:
+ - Template content that will be processed.
+ required: false
+ default: None
+ aliases: []
+ params:
+ description:
+ - A list of parameters that will be inserted into the template.
+ required: false
+ default: None
+ aliases: []
+ create:
+ description:
+ - Whether or not to create the template after being processed. e.g. oc process | oc create -f -
+ required: False
+ default: False
+ aliases: []
+ reconcile:
+ description:
+ - Whether or not to attempt to determine if there are updates or changes in the incoming template.
+ default: true
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: process the cloud volume provisioner template with variables
+ oc_process:
+ namespace: openshift-infra
+ template_name: online-volume-provisioner
+ create: True
+ params:
+ PLAT: rhel7
+ register: processout
+ run_once: true
+- debug: var=processout
+'''
+
+# -*- -*- -*- End included fragment: doc/process -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_process.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCProcess(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5. we need 6
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ namespace,
+ tname=None,
+ params=None,
+ create=False,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ tdata=None,
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCProcess, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = tname
+ self.data = tdata
+ self.params = params
+ self.create = create
+ self._template = None
+
+ @property
+ def template(self):
+ '''template property'''
+ if self._template is None:
+ results = self._process(self.name, False, self.params, self.data)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Error processing template [%s]: %s' %(self.name, results))
+ self._template = results['results']['items']
+
+ return self._template
+
+ def get(self):
+ '''get the template'''
+ results = self._get('template', self.name)
+ if results['returncode'] != 0:
+ # Does the template exist??
+ if 'not found' in results['stderr']:
+ results['returncode'] = 0
+ results['exists'] = False
+ results['results'] = []
+
+ return results
+
+ def delete(self, obj):
+ '''delete a resource'''
+ return self._delete(obj['kind'], obj['metadata']['name'])
+
+ def create_obj(self, obj):
+ '''create a resource'''
+ return self._create_from_content(obj['metadata']['name'], obj)
+
+ def process(self, create=None):
+ '''process a template'''
+ do_create = False
+ if create != None:
+ do_create = create
+ else:
+ do_create = self.create
+
+ return self._process(self.name, do_create, self.params, self.data)
+
+ def exists(self):
+ '''return whether the template exists'''
+ # Always return true if we're being passed template data
+ if self.data:
+ return True
+ t_results = self._get('template', self.name)
+
+ if t_results['returncode'] != 0:
+ # Does the template exist??
+ if 'not found' in t_results['stderr']:
+ return False
+ else:
+ raise OpenShiftCLIError('Something went wrong. %s' % t_results)
+
+ return True
+
+ def needs_update(self):
+ '''attempt to process the template and return it for comparison with oc objects'''
+ obj_results = []
+ for obj in self.template:
+
+ # build a list of types to skip
+ skip = []
+
+ if obj['kind'] == 'ServiceAccount':
+ skip.extend(['secrets', 'imagePullSecrets'])
+ if obj['kind'] == 'BuildConfig':
+ skip.extend(['lastTriggeredImageID'])
+ if obj['kind'] == 'ImageStream':
+ skip.extend(['generation'])
+ if obj['kind'] == 'DeploymentConfig':
+ skip.extend(['lastTriggeredImage'])
+
+ # fetch the current object
+ curr_obj_results = self._get(obj['kind'], obj['metadata']['name'])
+ if curr_obj_results['returncode'] != 0:
+ # Does the template exist??
+ if 'not found' in curr_obj_results['stderr']:
+ obj_results.append((obj, True))
+ continue
+
+ # check the generated object against the existing object
+ if not Utils.check_def_equal(obj, curr_obj_results['results'][0], skip_keys=skip):
+ obj_results.append((obj, True))
+ continue
+
+ obj_results.append((obj, False))
+
+ return obj_results
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ ocprocess = OCProcess(params['namespace'],
+ params['template_name'],
+ params['params'],
+ params['create'],
+ kubeconfig=params['kubeconfig'],
+ tdata=params['content'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocprocess.get()
+
+ if state == 'list':
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg" : api_rval}
+
+ return {"changed" : False, "results": api_rval, "state": state}
+
+ elif state == 'present':
+ if check_mode and params['create']:
+ return {"changed": True, 'msg': "CHECK_MODE: Would have processed template."}
+
+ if not ocprocess.exists() or not params['reconcile']:
+ #FIXME: this code will never get run in a way that succeeds when
+ # module.params['reconcile'] is true. Because oc_process doesn't
+ # create the actual template, the check of ocprocess.exists()
+ # is meaningless. Either it's already here and this code
+ # won't be run, or this code will fail because there is no
+ # template available for oc process to use. Have we conflated
+ # the template's existence with the existence of the objects
+ # it describes?
+
+ # Create it here
+ api_rval = ocprocess.process()
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ if params['create']:
+ return {"changed": True, "results": api_rval, "state": state}
+
+ return {"changed": False, "results": api_rval, "state": state}
+
+ # verify results
+ update = False
+ rval = []
+ all_results = ocprocess.needs_update()
+ for obj, status in all_results:
+ if status:
+ ocprocess.delete(obj)
+ results = ocprocess.create_obj(obj)
+ results['kind'] = obj['kind']
+ rval.append(results)
+ update = True
+
+ if not update:
+ return {"changed": update, "results": api_rval, "state": state}
+
+ for cmd in rval:
+ if cmd['returncode'] != 0:
+ return {"failed": True, "changed": update, "msg": rval, "state": state}
+
+ return {"changed": update, "results": rval, "state": state}
+
+
+# -*- -*- -*- End included fragment: class/oc_process.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_process.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for processing templates
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str', choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ template_name=dict(default=None, type='str'),
+ content=dict(default=None, type='str'),
+ params=dict(default=None, type='dict'),
+ create=dict(default=False, type='bool'),
+ reconcile=dict(default=True, type='bool'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCProcess.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_process.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_project.py b/roles/lib_openshift/library/oc_project.py
new file mode 100644
index 000000000..4eee748d7
--- /dev/null
+++ b/roles/lib_openshift/library/oc_project.py
@@ -0,0 +1,1732 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/project -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_project
+short_description: Module to manage openshift projects
+description:
+ - Manage openshift projects programmatically.
+options:
+ state:
+ description:
+ - If present, the project will be created if it doesn't exist or update if different. If absent, the project will be removed if present. If list, information about the project will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ display_name:
+ description:
+ - The display name attribute for a project
+ required: false
+ default: None
+ aliases: []
+ description:
+ description:
+ - The description attribute for a project
+ required: false
+ default: None
+ aliases: []
+ admin:
+ description:
+ - The project admin username
+ required: false
+ default: false
+ aliases: []
+ admin_role:
+ description:
+ - The project admin username
+ required: false
+ default: 'admin'
+ aliases: []
+ node_selector:
+ description:
+ - The node selector for this project.
+ - This allows certain pods in this project to run on certain nodes.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create secret
+ oc_project:
+ state: present
+ name: openshift-ops
+ display_name: operations team project
+ node_selector:
+ - top=secret
+ - noncustomer=True
+'''
+
+# -*- -*- -*- End included fragment: doc/project -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/project.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class ProjectConfig(OpenShiftCLIConfig):
+ ''' project config object '''
+ def __init__(self, rname, namespace, kubeconfig, project_options):
+ super(ProjectConfig, self).__init__(rname, None, kubeconfig, project_options)
+
+
+class Project(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ annotations_path = "metadata.annotations"
+ kind = 'Project'
+ annotation_prefix = 'openshift.io/'
+
+ def __init__(self, content):
+ '''Project constructor'''
+ super(Project, self).__init__(content=content)
+
+ def get_annotations(self):
+ ''' return the annotations'''
+ return self.get(Project.annotations_path) or {}
+
+ def add_annotations(self, inc_annos):
+ ''' add an annotation to the other annotations'''
+ if not isinstance(inc_annos, list):
+ inc_annos = [inc_annos]
+
+ annos = self.get_annotations()
+ if not annos:
+ self.put(Project.annotations_path, inc_annos)
+ else:
+ for anno in inc_annos:
+ for key, value in anno.items():
+ annos[key] = value
+
+ return True
+
+ def find_annotation(self, key):
+ ''' find an annotation'''
+ annotations = self.get_annotations()
+ for anno in annotations:
+ if Project.annotation_prefix + key == anno:
+ return annotations[anno]
+
+ return None
+
+ def delete_annotation(self, inc_anno_keys):
+ ''' remove an annotation from a project'''
+ if not isinstance(inc_anno_keys, list):
+ inc_anno_keys = [inc_anno_keys]
+
+ annos = self.get(Project.annotations_path) or {}
+
+ if not annos:
+ return True
+
+ removed = False
+ for inc_anno in inc_anno_keys:
+ anno = self.find_annotation(inc_anno)
+ if anno:
+ del annos[Project.annotation_prefix + anno]
+ removed = True
+
+ return removed
+
+ def update_annotation(self, key, value):
+ ''' remove an annotation for a project'''
+ annos = self.get(Project.annotations_path) or {}
+
+ if not annos:
+ return True
+
+ updated = False
+ anno = self.find_annotation(key)
+ if anno:
+ annos[Project.annotation_prefix + key] = value
+ updated = True
+
+ else:
+ self.add_annotations({Project.annotation_prefix + key: value})
+
+ return updated
+
+# -*- -*- -*- End included fragment: lib/project.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_project.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCProject(OpenShiftCLI):
+ ''' Project Class to manage project/namespace objects'''
+ kind = 'namespace'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCProject '''
+ super(OCProject, self).__init__(None, config.kubeconfig)
+ self.config = config
+ self._project = None
+
+ @property
+ def project(self):
+ ''' property for project'''
+ if not self._project:
+ self.get()
+ return self._project
+
+ @project.setter
+ def project(self, data):
+ ''' setter function for project propeorty'''
+ self._project = data
+
+ def exists(self):
+ ''' return whether a project exists '''
+ if self.project:
+ return True
+
+ return False
+
+ def get(self):
+ '''return project '''
+ result = self._get(self.kind, self.config.name)
+
+ if result['returncode'] == 0:
+ self.project = Project(content=result['results'][0])
+ result['results'] = self.project.yaml_dict
+
+ elif 'namespaces "%s" not found' % self.config.name in result['stderr']:
+ result = {'results': [], 'returncode': 0}
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create a project '''
+ cmd = ['new-project', self.config.name]
+ cmd.extend(self.config.to_option_list())
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ def update(self):
+ '''update a project '''
+
+ if self.config.config_options['display_name']['value'] is not None:
+ self.project.update_annotation('display-name', self.config.config_options['display_name']['value'])
+
+ if self.config.config_options['description']['value'] is not None:
+ self.project.update_annotation('description', self.config.config_options['description']['value'])
+
+ # work around for immutable project field
+ if self.config.config_options['node_selector']['value'] is not None:
+ self.project.update_annotation('node-selector', self.config.config_options['node_selector']['value'])
+
+ return self._replace_content(self.kind, self.config.name, self.project.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ if self.config.config_options['display_name']['value'] is not None:
+ result = self.project.find_annotation("display-name")
+ if result != self.config.config_options['display_name']['value']:
+ return True
+
+ if self.config.config_options['description']['value'] is not None:
+ result = self.project.find_annotation("description")
+ if result != self.config.config_options['description']['value']:
+ return True
+
+ if self.config.config_options['node_selector']['value'] is not None:
+ result = self.project.find_annotation("node-selector")
+ if result != self.config.config_options['node_selector']['value']:
+ return True
+
+ return False
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ node_selector = None
+ if params['node_selector'] is not None:
+ node_selector = ','.join(params['node_selector'])
+
+ pconfig = ProjectConfig(
+ params['name'],
+ 'None',
+ params['kubeconfig'],
+ {
+ 'admin': {'value': params['admin'], 'include': True},
+ 'admin_role': {'value': params['admin_role'], 'include': True},
+ 'description': {'value': params['description'], 'include': True},
+ 'display_name': {'value': params['display_name'], 'include': True},
+ 'node_selector': {'value': node_selector, 'include': True},
+ },
+ )
+
+ oadm_project = OCProject(pconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oadm_project.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oadm_project.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oadm_project.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oadm_project.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oadm_project.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oadm_project.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oadm_project.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = oadm_project.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oadm_project.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. [%s]' % state}
+
+# -*- -*- -*- End included fragment: class/oc_project.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_project.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for project
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, require=True, type='str'),
+ display_name=dict(default=None, type='str'),
+ node_selector=dict(default=None, type='list'),
+ description=dict(default=None, type='str'),
+ admin=dict(default=None, type='str'),
+ admin_role=dict(default='admin', type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCProject.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_project.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_pvc.py b/roles/lib_openshift/library/oc_pvc.py
new file mode 100644
index 000000000..2e73a7645
--- /dev/null
+++ b/roles/lib_openshift/library/oc_pvc.py
@@ -0,0 +1,1847 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/pvc -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_pvc
+short_description: Modify, and idempotently manage openshift persistent volume claims
+description:
+ - Modify openshift persistent volume claims programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a pvc
+ - absent - will remove a pvc
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ volume_capacity:
+ description:
+ - The requested volume capacity
+ required: False
+ default: 1G
+ aliases: []
+ access_modes:
+ description:
+ - The access modes allowed for the pvc
+ - Expects a list
+ required: False
+ default: ReadWriteOnce
+ choices:
+ - ReadWriteOnce
+ - ReadOnlyMany
+ - ReadWriteMany
+ aliases: []
+ storage_class_name:
+ description:
+ - The storage class name for the PVC
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - A hash of key/values for the matchLabels
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create a pvc
+ oc_pvc:
+ namespace: awesomeapp
+ name: dbstorage
+ access_modes:
+ - ReadWriteOnce
+ volume_capacity: 5G
+ register: pvcout
+'''
+
+# -*- -*- -*- End included fragment: doc/pvc -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/pvc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class PersistentVolumeClaimConfig(object):
+ ''' Handle pvc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ access_modes=None,
+ vol_capacity='1G',
+ selector=None,
+ storage_class_name=None):
+ ''' constructor for handling pvc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.access_modes = access_modes
+ self.vol_capacity = vol_capacity
+ self.data = {}
+ self.selector = selector
+ self.storage_class_name = storage_class_name
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'PersistentVolumeClaim'
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ # spec
+ self.data['spec'] = {}
+ self.data['spec']['accessModes'] = ['ReadWriteOnce']
+ if self.access_modes:
+ self.data['spec']['accessModes'] = self.access_modes
+ if self.selector:
+ self.data['spec']['selector'] = {'matchLabels': self.selector}
+
+ # storage capacity
+ self.data['spec']['resources'] = {}
+ self.data['spec']['resources']['requests'] = {}
+ self.data['spec']['resources']['requests']['storage'] = self.vol_capacity
+
+ if self.storage_class_name:
+ self.data['spec']['storageClassName'] = self.storage_class_name
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class PersistentVolumeClaim(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ access_modes_path = "spec.accessModes"
+ volume_capacity_path = "spec.requests.storage"
+ volume_name_path = "spec.volumeName"
+ bound_path = "status.phase"
+ kind = 'PersistentVolumeClaim'
+ selector_path = "spec.selector.matchLabels"
+ storage_class_name_path = "spec.storageClassName"
+
+ def __init__(self, content):
+ '''PersistentVolumeClaim constructor'''
+ super(PersistentVolumeClaim, self).__init__(content=content)
+ self._access_modes = None
+ self._volume_capacity = None
+ self._volume_name = None
+ self._selector = None
+ self._storage_class_name = None
+
+ @property
+ def storage_class_name(self):
+ ''' storage_class_name property '''
+ if self._storage_class_name is None:
+ self._storage_class_name = self.get_storage_class_name()
+ return self._storage_class_name
+
+ @storage_class_name.setter
+ def storage_class_name(self, data):
+ ''' storage_class_name property setter'''
+ self._storage_class_name = data
+
+ @property
+ def volume_name(self):
+ ''' volume_name property '''
+ if self._volume_name is None:
+ self._volume_name = self.get_volume_name()
+ return self._volume_name
+
+ @volume_name.setter
+ def volume_name(self, data):
+ ''' volume_name property setter'''
+ self._volume_name = data
+
+ @property
+ def selector(self):
+ ''' selector property '''
+ if self._selector is None:
+ self._selector = self.get_selector()
+ if not isinstance(self._selector, dict):
+ self._selector = dict(self._selector)
+
+ return self._selector
+
+ @selector.setter
+ def selector(self, data):
+ ''' selector property setter'''
+ if not isinstance(data, dict):
+ data = dict(data)
+
+ self._selector = data
+
+ @property
+ def access_modes(self):
+ ''' access_modes property '''
+ if self._access_modes is None:
+ self._access_modes = self.get_access_modes()
+ if not isinstance(self._access_modes, list):
+ self._access_modes = list(self._access_modes)
+
+ return self._access_modes
+
+ @access_modes.setter
+ def access_modes(self, data):
+ ''' access_modes property setter'''
+ if not isinstance(data, list):
+ data = list(data)
+
+ self._access_modes = data
+
+ @property
+ def volume_capacity(self):
+ ''' volume_capacity property '''
+ if self._volume_capacity is None:
+ self._volume_capacity = self.get_volume_capacity()
+ return self._volume_capacity
+
+ @volume_capacity.setter
+ def volume_capacity(self, data):
+ ''' volume_capacity property setter'''
+ self._volume_capacity = data
+
+ def get_storage_class_name(self):
+ '''get storage_class_name'''
+ return self.get(PersistentVolumeClaim.storage_class_name_path) or []
+
+ def get_selector(self):
+ '''get selector'''
+ return self.get(PersistentVolumeClaim.selector_path) or []
+
+ def get_access_modes(self):
+ '''get access_modes'''
+ return self.get(PersistentVolumeClaim.access_modes_path) or []
+
+ def get_volume_capacity(self):
+ '''get volume_capacity'''
+ return self.get(PersistentVolumeClaim.volume_capacity_path) or []
+
+ def get_volume_name(self):
+ '''get volume_name'''
+ return self.get(PersistentVolumeClaim.volume_name_path) or []
+
+ def is_bound(self):
+ '''return whether volume is bound'''
+ return self.get(PersistentVolumeClaim.bound_path) or []
+
+ #### ADD #####
+ def add_access_mode(self, inc_mode):
+ ''' add an access_mode'''
+ if self.access_modes:
+ self.access_modes.append(inc_mode)
+ else:
+ self.put(PersistentVolumeClaim.access_modes_path, [inc_mode])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_access_mode(self, inc_mode):
+ ''' remove an access_mode'''
+ try:
+ self.access_modes.remove(inc_mode)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_access_mode(self, inc_mode):
+ ''' update an access_mode'''
+ try:
+ index = self.access_modes.index(inc_mode)
+ except ValueError as _:
+ return self.add_access_mode(inc_mode)
+
+ self.access_modes[index] = inc_mode
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_access_mode(self, inc_mode):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.access_modes.index(inc_mode)
+ except ValueError as _:
+ return index
+
+ return index
+
+# -*- -*- -*- End included fragment: lib/pvc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_pvc.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCPVC(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'pvc'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCPVC, self).__init__(config.namespace, config.kubeconfig)
+ self.config = config
+ self.namespace = config.namespace
+ self._pvc = None
+
+ @property
+ def pvc(self):
+ ''' property function pvc'''
+ if not self._pvc:
+ self.get()
+ return self._pvc
+
+ @pvc.setter
+ def pvc(self, data):
+ ''' setter function for yedit var '''
+ self._pvc = data
+
+ def bound(self):
+ '''return whether the pvc is bound'''
+ if self.pvc.get_volume_name():
+ return True
+
+ return False
+
+ def exists(self):
+ ''' return whether a pvc exists '''
+ if self.pvc:
+ return True
+
+ return False
+
+ def get(self):
+ '''return pvc information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.pvc = PersistentVolumeClaim(content=result['results'][0])
+ elif '\"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ # need to update the tls information and the service name
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ if self.pvc.get_volume_name() or self.pvc.is_bound():
+ return False
+
+ skip = []
+ return not Utils.check_def_equal(self.config.data, self.pvc.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+ pconfig = PersistentVolumeClaimConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['access_modes'],
+ params['volume_capacity'],
+ params['selector'],
+ params['storage_class_name'],
+ )
+ oc_pvc = OCPVC(pconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_pvc.get()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_pvc.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_pvc.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_pvc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_pvc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_pvc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_pvc.pvc.is_bound() or oc_pvc.pvc.get_volume_name():
+ api_rval['msg'] = '##### - This volume is currently bound. Will not update - ####'
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if oc_pvc.needs_update():
+ api_rval = oc_pvc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_pvc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_pvc.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_pvc.py -*- -*- -*-
+
+#pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for pvc
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ volume_capacity=dict(default='1G', type='str'),
+ storage_class_name=dict(default=None, required=False, type='str'),
+ selector=dict(default=None, required=False, type='dict'),
+ access_modes=dict(default=['ReadWriteOnce'], type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCPVC.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_pvc.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_route.py b/roles/lib_openshift/library/oc_route.py
new file mode 100644
index 000000000..e003770d8
--- /dev/null
+++ b/roles/lib_openshift/library/oc_route.py
@@ -0,0 +1,1869 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/route -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_route
+short_description: Create, modify, and idempotently manage openshift routes.
+description:
+ - Manage openshift route objects programmatically.
+options:
+ state:
+ description:
+ - State represents whether to create, modify, delete, or list
+ required: true
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ labels:
+ description:
+ - The labels to apply on the route
+ required: false
+ default: None
+ aliases: []
+ tls_termination:
+ description:
+ - The options for termination. e.g. reencrypt
+ required: false
+ default: None
+ aliases: []
+ dest_cacert_path:
+ description:
+ - The path to the dest_cacert
+ required: false
+ default: None
+ aliases: []
+ cacert_path:
+ description:
+ - The path to the cacert
+ required: false
+ default: None
+ aliases: []
+ cert_path:
+ description:
+ - The path to the cert
+ required: false
+ default: None
+ aliases: []
+ key_path:
+ description:
+ - The path to the key
+ required: false
+ default: None
+ aliases: []
+ dest_cacert_content:
+ description:
+ - The dest_cacert content
+ required: false
+ default: None
+ aliases: []
+ cacert_content:
+ description:
+ - The cacert content
+ required: false
+ default: None
+ aliases: []
+ cert_content:
+ description:
+ - The cert content
+ required: false
+ default: None
+ aliases: []
+ service_name:
+ description:
+ - The name of the service that this route points to.
+ required: false
+ default: None
+ aliases: []
+ host:
+ description:
+ - The host that the route will use. e.g. myapp.x.y.z
+ required: false
+ default: None
+ aliases: []
+ port:
+ description:
+ - The Name of the service port or number of the container port the route will route traffic to
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Configure certificates for reencrypt route
+ oc_route:
+ name: myapproute
+ namespace: awesomeapp
+ cert_path: "/etc/origin/master/named_certificates/myapp_cert
+ key_path: "/etc/origin/master/named_certificates/myapp_key
+ cacert_path: "/etc/origin/master/named_certificates/myapp_cacert
+ dest_cacert_content: "{{ dest_cacert_content }}"
+ service_name: myapp_php
+ host: myapp.awesomeapp.openshift.com
+ tls_termination: reencrypt
+ run_once: true
+'''
+
+# -*- -*- -*- End included fragment: doc/route -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/route.py -*- -*- -*-
+# noqa: E302,E301
+
+
+# pylint: disable=too-many-instance-attributes
+class RouteConfig(object):
+ ''' Handle route options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ labels=None,
+ destcacert=None,
+ cacert=None,
+ cert=None,
+ key=None,
+ host=None,
+ tls_termination=None,
+ service_name=None,
+ wildcard_policy=None,
+ weight=None,
+ port=None):
+ ''' constructor for handling route options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.labels = labels
+ self.host = host
+ self.tls_termination = tls_termination
+ self.destcacert = destcacert
+ self.cacert = cacert
+ self.cert = cert
+ self.key = key
+ self.service_name = service_name
+ self.port = port
+ self.data = {}
+ self.wildcard_policy = wildcard_policy
+ if wildcard_policy is None:
+ self.wildcard_policy = 'None'
+ self.weight = weight
+ if weight is None:
+ self.weight = 100
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Route'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ if self.labels:
+ self.data['metadata']['labels'] = self.labels
+ self.data['spec'] = {}
+
+ self.data['spec']['host'] = self.host
+
+ if self.tls_termination:
+ self.data['spec']['tls'] = {}
+
+ self.data['spec']['tls']['termination'] = self.tls_termination
+
+ if self.tls_termination != 'passthrough':
+ self.data['spec']['tls']['key'] = self.key
+ self.data['spec']['tls']['caCertificate'] = self.cacert
+ self.data['spec']['tls']['certificate'] = self.cert
+
+ if self.tls_termination == 'reencrypt':
+ self.data['spec']['tls']['destinationCACertificate'] = self.destcacert
+
+ self.data['spec']['to'] = {'kind': 'Service',
+ 'name': self.service_name,
+ 'weight': self.weight}
+
+ self.data['spec']['wildcardPolicy'] = self.wildcard_policy
+
+ if self.port:
+ self.data['spec']['port'] = {}
+ self.data['spec']['port']['targetPort'] = self.port
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class Route(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ wildcard_policy = "spec.wildcardPolicy"
+ host_path = "spec.host"
+ port_path = "spec.port.targetPort"
+ service_path = "spec.to.name"
+ weight_path = "spec.to.weight"
+ cert_path = "spec.tls.certificate"
+ cacert_path = "spec.tls.caCertificate"
+ destcacert_path = "spec.tls.destinationCACertificate"
+ termination_path = "spec.tls.termination"
+ key_path = "spec.tls.key"
+ kind = 'route'
+
+ def __init__(self, content):
+ '''Route constructor'''
+ super(Route, self).__init__(content=content)
+
+ def get_destcacert(self):
+ ''' return cert '''
+ return self.get(Route.destcacert_path)
+
+ def get_cert(self):
+ ''' return cert '''
+ return self.get(Route.cert_path)
+
+ def get_key(self):
+ ''' return key '''
+ return self.get(Route.key_path)
+
+ def get_cacert(self):
+ ''' return cacert '''
+ return self.get(Route.cacert_path)
+
+ def get_service(self):
+ ''' return service name '''
+ return self.get(Route.service_path)
+
+ def get_weight(self):
+ ''' return service weight '''
+ return self.get(Route.weight_path)
+
+ def get_termination(self):
+ ''' return tls termination'''
+ return self.get(Route.termination_path)
+
+ def get_host(self):
+ ''' return host '''
+ return self.get(Route.host_path)
+
+ def get_port(self):
+ ''' return port '''
+ return self.get(Route.port_path)
+
+ def get_wildcard_policy(self):
+ ''' return wildcardPolicy '''
+ return self.get(Route.wildcard_policy)
+
+# -*- -*- -*- End included fragment: lib/route.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_route.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCRoute(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'route'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCRoute, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self._route = None
+
+ @property
+ def route(self):
+ ''' property function for route'''
+ if not self._route:
+ self.get()
+ return self._route
+
+ @route.setter
+ def route(self, data):
+ ''' setter function for route '''
+ self._route = data
+
+ def exists(self):
+ ''' return whether a route exists '''
+ if self.route:
+ return True
+
+ return False
+
+ def get(self):
+ '''return route information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.route = Route(content=result['results'][0])
+ elif 'routes \"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ return self._replace_content(self.kind,
+ self.config.name,
+ self.config.data,
+ force=(self.config.host != self.route.get_host()))
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = []
+ return not Utils.check_def_equal(self.config.data, self.route.yaml_dict, skip_keys=skip, debug=self.verbose)
+
+ @staticmethod
+ def get_cert_data(path, content):
+ '''get the data for a particular value'''
+ rval = None
+ if path and os.path.exists(path) and os.access(path, os.R_OK):
+ rval = open(path).read()
+ elif content:
+ rval = content
+
+ return rval
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent asnible code
+
+ params comes from the ansible portion for this module
+ files: a dictionary for the certificates
+ {'cert': {'path': '',
+ 'content': '',
+ 'value': ''
+ }
+ }
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+ files = {'destcacert': {'path': params['dest_cacert_path'],
+ 'content': params['dest_cacert_content'],
+ 'value': None, },
+ 'cacert': {'path': params['cacert_path'],
+ 'content': params['cacert_content'],
+ 'value': None, },
+ 'cert': {'path': params['cert_path'],
+ 'content': params['cert_content'],
+ 'value': None, },
+ 'key': {'path': params['key_path'],
+ 'content': params['key_content'],
+ 'value': None, }, }
+
+ if params['tls_termination'] and params['tls_termination'].lower() != 'passthrough': # E501
+
+ for key, option in files.items():
+ if not option['path'] and not option['content']:
+ continue
+
+ option['value'] = OCRoute.get_cert_data(option['path'], option['content']) # E501
+
+ if not option['value']:
+ return {'failed': True,
+ 'msg': 'Verify that you pass a correct value for %s' % key}
+
+ rconfig = RouteConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['labels'],
+ files['destcacert']['value'],
+ files['cacert']['value'],
+ files['cert']['value'],
+ files['key']['value'],
+ params['host'],
+ params['tls_termination'],
+ params['service_name'],
+ params['wildcard_policy'],
+ params['weight'],
+ params['port'])
+
+ oc_route = OCRoute(rconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_route.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False,
+ 'results': api_rval['results'],
+ 'state': 'list'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_route.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'} # noqa: E501
+
+ api_rval = oc_route.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"} # noqa: E501
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_route.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} # noqa: E501
+
+ # Create it here
+ api_rval = oc_route.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ # return the created object
+ api_rval = oc_route.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ return {'changed': True, 'results': api_rval, 'state': "present"} # noqa: E501
+
+ ########
+ # Update
+ ########
+ if oc_route.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'} # noqa: E501
+
+ api_rval = oc_route.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ # return the created object
+ api_rval = oc_route.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ return {'changed': True, 'results': api_rval, 'state': "present"} # noqa: E501
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+ # catch all
+ return {'failed': True, 'msg': "Unknown State passed"}
+
+# -*- -*- -*- End included fragment: class/oc_route.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_route.py -*- -*- -*-
+
+
+# pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for route
+ '''
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ labels=dict(default=None, type='dict'),
+ name=dict(default=None, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ tls_termination=dict(default=None, type='str'),
+ dest_cacert_path=dict(default=None, type='str'),
+ cacert_path=dict(default=None, type='str'),
+ cert_path=dict(default=None, type='str'),
+ key_path=dict(default=None, type='str'),
+ dest_cacert_content=dict(default=None, type='str'),
+ cacert_content=dict(default=None, type='str'),
+ cert_content=dict(default=None, type='str'),
+ key_content=dict(default=None, type='str'),
+ service_name=dict(default=None, type='str'),
+ host=dict(default=None, type='str'),
+ wildcard_policy=dict(default=None, type='str'),
+ weight=dict(default=None, type='int'),
+ port=dict(default=None, type='int'),
+ ),
+ mutually_exclusive=[('dest_cacert_path', 'dest_cacert_content'),
+ ('cacert_path', 'cacert_content'),
+ ('cert_path', 'cert_content'),
+ ('key_path', 'key_content'), ],
+ supports_check_mode=True,
+ )
+
+ results = OCRoute.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_route.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_scale.py b/roles/lib_openshift/library/oc_scale.py
new file mode 100644
index 000000000..c142f1f43
--- /dev/null
+++ b/roles/lib_openshift/library/oc_scale.py
@@ -0,0 +1,1918 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/scale -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_scale
+short_description: Manage openshift services through the scale parameters
+description:
+ - Manage openshift services through scaling them.
+options:
+ state:
+ description:
+ - State represents whether to scale or list the current replicas
+ required: true
+ default: present
+ choices: ["present", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ kind:
+ description:
+ - The kind of object to scale.
+ required: false
+ default: None
+ choices:
+ - rc
+ - dc
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: scale down a rc to 0
+ oc_scale:
+ name: my-replication-controller
+ kind: rc
+ namespace: openshift-infra
+ replicas: 0
+
+- name: scale up a deploymentconfig to 2
+ oc_scale:
+ name: php
+ kind: dc
+ namespace: my-php-app
+ replicas: 2
+'''
+
+# -*- -*- -*- End included fragment: doc/scale -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
+
+# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/replicationcontroller.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class ReplicationController(DeploymentConfig):
+ ''' Class to model a replicationcontroller openshift object.
+
+ Currently we are modeled after a deployment config since they
+ are very similar. In the future, when the need arises we
+ will add functionality to this class.
+ '''
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content):
+ ''' Constructor for ReplicationController '''
+ super(ReplicationController, self).__init__(content=content)
+
+# -*- -*- -*- End included fragment: lib/replicationcontroller.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_scale.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCScale(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ resource_name,
+ namespace,
+ replicas,
+ kind,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCScale '''
+ super(OCScale, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.kind = kind
+ self.replicas = replicas
+ self.name = resource_name
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var '''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var '''
+ self._resource = data
+
+ def get(self):
+ '''return replicas information '''
+ vol = self._get(self.kind, self.name)
+ if vol['returncode'] == 0:
+ if self.kind == 'dc':
+ # The resource returned from a query could be an rc or dc.
+ # pylint: disable=redefined-variable-type
+ self.resource = DeploymentConfig(content=vol['results'][0])
+ vol['results'] = [self.resource.get_replicas()]
+ if self.kind == 'rc':
+ # The resource returned from a query could be an rc or dc.
+ # pylint: disable=redefined-variable-type
+ self.resource = ReplicationController(content=vol['results'][0])
+ vol['results'] = [self.resource.get_replicas()]
+
+ return vol
+
+ def put(self):
+ '''update replicas into dc '''
+ self.resource.update_replicas(self.replicas)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def needs_update(self):
+ ''' verify whether an update is needed '''
+ return self.resource.needs_update_replicas(self.replicas)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''perform the idempotent ansible logic'''
+
+ oc_scale = OCScale(params['name'],
+ params['namespace'],
+ params['replicas'],
+ params['kind'],
+ params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_scale.get()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'result': api_rval['results'], 'state': 'list'} # noqa: E501
+
+ elif state == 'present':
+ ########
+ # Update
+ ########
+ if oc_scale.needs_update():
+ if check_mode:
+ return {'changed': True, 'result': 'CHECK_MODE: Would have updated.'} # noqa: E501
+ api_rval = oc_scale.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_scale.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
+
+ return {'changed': False, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
+
+ return {'failed': True, 'msg': 'Unknown state passed. [{}]'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_scale.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_scale.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for scaling
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str', choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='dc', choices=['dc', 'rc'], type='str'),
+ namespace=dict(default='default', type='str'),
+ replicas=dict(default=None, type='int'),
+ name=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+ rval = OCScale.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_scale.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_secret.py b/roles/lib_openshift/library/oc_secret.py
new file mode 100644
index 000000000..0614f359d
--- /dev/null
+++ b/roles/lib_openshift/library/oc_secret.py
@@ -0,0 +1,1818 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/secret -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_secret
+short_description: Module to manage openshift secrets
+description:
+ - Manage openshift secrets programmatically.
+options:
+ state:
+ description:
+ - If present, the secret will be created if it doesn't exist or updated if different. If absent, the secret will be removed if present. If list, information about the secret will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ annotations:
+ description:
+ - Annotations to apply to the object
+ required: false
+ default: None
+ aliases: []
+ files:
+ description:
+ - A list of files provided for secrets
+ required: false
+ default: None
+ aliases: []
+ delete_after:
+ description:
+ - Whether or not to delete the files after processing them.
+ required: false
+ default: false
+ aliases: []
+ contents:
+ description:
+ - Content of the secrets
+ required: false
+ default: None
+ aliases: []
+ type:
+ description:
+ - The secret type.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to force the operation
+ required: false
+ default: false
+ aliases: []
+ decode:
+ description:
+ - base64 decode the object
+ required: false
+ default: false
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create secret
+ oc_secret:
+ state: present
+ namespace: openshift-infra
+ name: metrics-deployer
+ files:
+ - name: nothing
+ path: /dev/null
+ register: secretout
+ run_once: true
+
+- name: get ca from hawkular
+ oc_secret:
+ state: list
+ namespace: openshift-infra
+ name: hawkular-metrics-certificate
+ decode: True
+ register: hawkout
+ run_once: true
+
+- name: Create secrets
+ oc_secret:
+ namespace: mynamespace
+ name: mysecrets
+ contents:
+ - path: data.yml
+ data: "{{ data_content }}"
+ - path: auth-keys
+ data: "{{ auth_keys_content }}"
+ - path: configdata.yml
+ data: "{{ configdata_content }}"
+ - path: cert.crt
+ data: "{{ cert_content }}"
+ - path: key.pem
+ data: "{{ osso_site_key_content }}"
+ - path: ca.cert.pem
+ data: "{{ ca_cert_content }}"
+ register: secretout
+'''
+
+# -*- -*- -*- End included fragment: doc/secret -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/secret.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class SecretConfig(object):
+ ''' Handle secret options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ secrets=None,
+ stype=None,
+ annotations=None):
+ ''' constructor for handling secret options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.type = stype
+ self.namespace = namespace
+ self.secrets = secrets
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a secret dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Secret'
+ self.data['type'] = self.type
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ self.data['data'] = {}
+ if self.secrets:
+ for key, value in self.secrets.items():
+ self.data['data'][key] = value
+ if self.annotations:
+ self.data['metadata']['annotations'] = self.annotations
+
+# pylint: disable=too-many-instance-attributes
+class Secret(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ secret_path = "data"
+ kind = 'secret'
+
+ def __init__(self, content):
+ '''secret constructor'''
+ super(Secret, self).__init__(content=content)
+ self._secrets = None
+
+ @property
+ def secrets(self):
+ '''secret property getter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self):
+ '''secret property setter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ def get_secrets(self):
+ ''' returns all of the defined secrets '''
+ return self.get(Secret.secret_path) or {}
+
+ def add_secret(self, key, value):
+ ''' add a secret '''
+ if self.secrets:
+ self.secrets[key] = value
+ else:
+ self.put(Secret.secret_path, {key: value})
+
+ return True
+
+ def delete_secret(self, key):
+ ''' delete secret'''
+ try:
+ del self.secrets[key]
+ except KeyError as _:
+ return False
+
+ return True
+
+ def find_secret(self, key):
+ ''' find secret'''
+ rval = None
+ try:
+ rval = self.secrets[key]
+ except KeyError as _:
+ return None
+
+ return {'key': key, 'value': rval}
+
+ def update_secret(self, key, value):
+ ''' update a secret'''
+ if key in self.secrets:
+ self.secrets[key] = value
+ else:
+ self.add_secret(key, value)
+
+ return True
+
+# -*- -*- -*- End included fragment: lib/secret.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_secret.py -*- -*- -*-
+
+
+# pylint: disable=wrong-import-position,wrong-import-order
+import base64
+
+# pylint: disable=too-many-arguments
+class OCSecret(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools
+ '''
+ def __init__(self,
+ namespace,
+ secret_name=None,
+ secret_type=None,
+ decode=False,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCSecret, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = secret_name
+ self.type = secret_type
+ self.decode = decode
+
+ def get(self):
+ '''return a secret by name '''
+ results = self._get('secrets', self.name)
+ results['decoded'] = {}
+ results['exists'] = False
+ if results['returncode'] == 0 and results['results'][0]:
+ results['exists'] = True
+ if self.decode:
+ if 'data' in results['results'][0]:
+ for sname, value in results['results'][0]['data'].items():
+ results['decoded'][sname] = base64.b64decode(value)
+
+ if results['returncode'] != 0 and '"%s" not found' % self.name in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete a secret by name'''
+ return self._delete('secrets', self.name)
+
+ def create(self, files=None, contents=None, force=False):
+ '''Create a secret '''
+ if not files:
+ files = Utils.create_tmp_files_from_contents(contents)
+
+ secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files]
+ cmd = ['secrets', 'new', self.name]
+ if self.type is not None:
+ cmd.append("--type=%s" % (self.type))
+ if force:
+ cmd.append('--confirm')
+ cmd.extend(secrets)
+
+ results = self.openshift_cmd(cmd)
+
+ return results
+
+ def update(self, files, force=False):
+ '''run update secret
+
+ This receives a list of file names and converts it into a secret.
+ The secret is then written to disk and passed into the `oc replace` command.
+ '''
+ secret = self.prep_secret(files, force)
+ if secret['returncode'] != 0:
+ return secret
+
+ sfile_path = '/tmp/%s' % self.name
+ with open(sfile_path, 'w') as sfd:
+ sfd.write(json.dumps(secret['results']))
+
+ atexit.register(Utils.cleanup, [sfile_path])
+
+ return self._replace(sfile_path, force=force)
+
+ def prep_secret(self, files=None, contents=None, force=False):
+ ''' return what the secret would look like if created
+ This is accomplished by passing -ojson. This will most likely change in the future
+ '''
+ if not files:
+ files = Utils.create_tmp_files_from_contents(contents)
+
+ secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files]
+ cmd = ['-ojson', 'secrets', 'new', self.name]
+ if self.type is not None:
+ cmd.extend(["--type=%s" % (self.type)])
+ if force:
+ cmd.append('--confirm')
+ cmd.extend(secrets)
+
+ return self.openshift_cmd(cmd, output=True)
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ ocsecret = OCSecret(params['namespace'],
+ params['name'],
+ params['type'],
+ params['decode'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocsecret.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, state: 'list'}
+
+ if not params['name']:
+ return {'failed': True,
+ 'msg': 'Please specify a name when state is absent|present.'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a delete.'}
+
+ api_rval = ocsecret.delete()
+ return {'changed': True, 'results': api_rval, 'state': 'absent'}
+
+ if state == 'present':
+ if params['files']:
+ files = params['files']
+ elif params['contents']:
+ files = Utils.create_tmp_files_from_contents(params['contents'])
+ else:
+ files = [{'name': 'null', 'path': os.devnull}]
+
+ ########
+ # Create
+ ########
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'Would have performed a create.'}
+
+ api_rval = ocsecret.create(files, params['contents'], force=params['force'])
+
+ # Remove files
+ if files and params['delete_after']:
+ Utils.cleanup([ftmp['path'] for ftmp in files])
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True,
+ 'msg': api_rval}
+
+ return {'changed': True,
+ 'results': api_rval,
+ 'state': 'present'}
+
+ ########
+ # Update
+ ########
+ secret = ocsecret.prep_secret(params['files'], params['contents'], force=params['force'])
+
+ if secret['returncode'] != 0:
+ return {'failed': True, 'msg': secret}
+
+ if Utils.check_def_equal(secret['results'], api_rval['results'][0]):
+
+ # Remove files
+ if files and params['delete_after']:
+ Utils.cleanup([ftmp['path'] for ftmp in files])
+
+ return {'changed': False,
+ 'results': secret['results'],
+ 'state': 'present'}
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'Would have performed an update.'}
+
+ api_rval = ocsecret.update(files, force=params['force'])
+
+ # Remove files
+ if secret and params['delete_after']:
+ Utils.cleanup([ftmp['path'] for ftmp in files])
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True,
+ 'msg': api_rval}
+
+ return {'changed': True,
+ 'results': api_rval,
+ 'state': 'present'}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_secret.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_secret.py -*- -*- -*-
+
+
+def main():
+ '''
+ ansible oc module for managing OpenShift Secrets
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, type='str'),
+ annotations=dict(default=None, type='dict'),
+ type=dict(default=None, type='str'),
+ files=dict(default=None, type='list'),
+ delete_after=dict(default=False, type='bool'),
+ contents=dict(default=None, type='list'),
+ force=dict(default=False, type='bool'),
+ decode=dict(default=False, type='bool'),
+ ),
+ mutually_exclusive=[["contents", "files"]],
+
+ supports_check_mode=True,
+ )
+
+
+ rval = OCSecret.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_secret.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_service.py b/roles/lib_openshift/library/oc_service.py
new file mode 100644
index 000000000..3e8aea4f1
--- /dev/null
+++ b/roles/lib_openshift/library/oc_service.py
@@ -0,0 +1,1865 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/service -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_service
+short_description: Create, modify, and idempotently manage openshift services.
+description:
+ - Manage openshift service objects programmatically.
+options:
+ state:
+ description:
+ - State represents whether to create, modify, delete, or list
+ required: False
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ selector:
+ description:
+ - The selector to apply when filtering for services.
+ required: false
+ default: None
+ aliases: []
+ labels:
+ description:
+ - The labels to apply on the service.
+ required: false
+ default: None
+ aliases: []
+ clusterip:
+ description:
+ - The cluster ip address to use with this service.
+ required: false
+ default: None
+ aliases: []
+ portalip:
+ description:
+ - The portal ip(virtual ip) address to use with this service.
+ - "https://docs.openshift.com/enterprise/3.0/architecture/core_concepts/pods_and_services.html#services"
+ required: false
+ default: None
+ aliases: []
+ ports:
+ description:
+ - A list of the ports that are used for this service. This includes name, port, protocol, and targetPort.
+ - See examples.
+ required: false
+ default: None
+ aliases: []
+ session_affinity:
+ description:
+ - The type of session affinity to use.
+ required: false
+ default: 'None'
+ aliases: []
+ service_type:
+ description:
+ - The type of service desired. Each option tells the service to behave accordingly.
+ - https://kubernetes.io/docs/user-guide/services/
+ required: false
+ default: ClusterIP
+ choices:
+ - ClusterIP
+ - NodePort
+ - LoadBalancer
+ - ExternalName
+ aliases: []
+ externalips:
+ description:
+ - A list of the external IPs that are exposed for this service.
+ - https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: get docker-registry service
+ run_once: true
+ oc_service:
+ namespace: default
+ name: docker-registry
+ state: list
+ register: registry_service_out
+
+- name: create the docker-registry service
+ oc_service:
+ namespace: default
+ name: docker-registry
+ ports:
+ - name: 5000-tcp
+ port: 5000
+ protocol: TCP
+ targetPort: 5000
+ selector:
+ docker-registry: default
+ session_affinity: ClientIP
+ service_type: ClusterIP
+ register: svc_out
+ notify:
+ - restart openshift master services
+'''
+
+# -*- -*- -*- End included fragment: doc/service -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/service.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class ServiceConfig(object):
+ ''' Handle service options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ ports,
+ selector=None,
+ labels=None,
+ cluster_ip=None,
+ portal_ip=None,
+ session_affinity=None,
+ service_type=None,
+ external_ips=None):
+ ''' constructor for handling service options '''
+ self.name = sname
+ self.namespace = namespace
+ self.ports = ports
+ self.selector = selector
+ self.labels = labels
+ self.cluster_ip = cluster_ip
+ self.portal_ip = portal_ip
+ self.session_affinity = session_affinity
+ self.service_type = service_type
+ self.external_ips = external_ips
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiates a service dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Service'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ if self.labels:
+ self.data['metadata']['labels'] = {}
+ for lab, lab_value in self.labels.items():
+ self.data['metadata']['labels'][lab] = lab_value
+ self.data['spec'] = {}
+
+ if self.ports:
+ self.data['spec']['ports'] = self.ports
+ else:
+ self.data['spec']['ports'] = []
+
+ if self.selector:
+ self.data['spec']['selector'] = self.selector
+
+ self.data['spec']['sessionAffinity'] = self.session_affinity or 'None'
+
+ if self.cluster_ip:
+ self.data['spec']['clusterIP'] = self.cluster_ip
+
+ if self.portal_ip:
+ self.data['spec']['portalIP'] = self.portal_ip
+
+ if self.service_type:
+ self.data['spec']['type'] = self.service_type
+
+ if self.external_ips:
+ self.data['spec']['externalIPs'] = self.external_ips
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class Service(Yedit):
+ ''' Class to model the oc service object '''
+ port_path = "spec.ports"
+ portal_ip = "spec.portalIP"
+ cluster_ip = "spec.clusterIP"
+ selector_path = 'spec.selector'
+ kind = 'Service'
+ external_ips = "spec.externalIPs"
+
+ def __init__(self, content):
+ '''Service constructor'''
+ super(Service, self).__init__(content=content)
+
+ def get_ports(self):
+ ''' get a list of ports '''
+ return self.get(Service.port_path) or []
+
+ def get_selector(self):
+ ''' get the service selector'''
+ return self.get(Service.selector_path) or {}
+
+ def add_ports(self, inc_ports):
+ ''' add a port object to the ports list '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get_ports()
+ if not ports:
+ self.put(Service.port_path, inc_ports)
+ else:
+ ports.extend(inc_ports)
+
+ return True
+
+ def find_ports(self, inc_port):
+ ''' find a specific port '''
+ for port in self.get_ports():
+ if port['port'] == inc_port['port']:
+ return port
+
+ return None
+
+ def delete_ports(self, inc_ports):
+ ''' remove a port from a service '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get(Service.port_path) or []
+
+ if not ports:
+ return True
+
+ removed = False
+ for inc_port in inc_ports:
+ port = self.find_ports(inc_port)
+ if port:
+ ports.remove(port)
+ removed = True
+
+ return removed
+
+ def add_cluster_ip(self, sip):
+ '''add cluster ip'''
+ self.put(Service.cluster_ip, sip)
+
+ def add_portal_ip(self, pip):
+ '''add cluster ip'''
+ self.put(Service.portal_ip, pip)
+
+ def get_external_ips(self):
+ ''' get a list of external_ips '''
+ return self.get(Service.external_ips) or []
+
+ def add_external_ips(self, inc_external_ips):
+ ''' add an external_ip to the external_ips list '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get_external_ips()
+ if not external_ips:
+ self.put(Service.external_ips, inc_external_ips)
+ else:
+ external_ips.extend(inc_external_ips)
+
+ return True
+
+ def find_external_ips(self, inc_external_ip):
+ ''' find a specific external IP '''
+ val = None
+ try:
+ idx = self.get_external_ips().index(inc_external_ip)
+ val = self.get_external_ips()[idx]
+ except ValueError:
+ pass
+
+ return val
+
+ def delete_external_ips(self, inc_external_ips):
+ ''' remove an external IP from a service '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get(Service.external_ips) or []
+
+ if not external_ips:
+ return True
+
+ removed = False
+ for inc_external_ip in inc_external_ips:
+ external_ip = self.find_external_ips(inc_external_ip)
+ if external_ip:
+ external_ips.remove(external_ip)
+ removed = True
+
+ return removed
+
+# -*- -*- -*- End included fragment: lib/service.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_service.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCService(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'service'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ labels,
+ selector,
+ cluster_ip,
+ portal_ip,
+ ports,
+ session_affinity,
+ service_type,
+ external_ips,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCService, self).__init__(namespace, kubeconfig, verbose)
+ self.namespace = namespace
+ self.config = ServiceConfig(sname, namespace, ports, selector, labels,
+ cluster_ip, portal_ip, session_affinity, service_type,
+ external_ips)
+ self.user_svc = Service(content=self.config.data)
+ self.svc = None
+
+ @property
+ def service(self):
+ ''' property function service'''
+ if not self.svc:
+ self.get()
+ return self.svc
+
+ @service.setter
+ def service(self, data):
+ ''' setter function for service var '''
+ self.svc = data
+
+ def exists(self):
+ ''' return whether a service exists '''
+ if self.service:
+ return True
+
+ return False
+
+ def get(self):
+ '''return service information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.service = Service(content=result['results'][0])
+ result['clusterip'] = self.service.get('spec.clusterIP')
+ elif 'services \"%s\" not found' % self.config.name in result['stderr']:
+ result['clusterip'] = ''
+ result['returncode'] = 0
+
+ return result
+
+ def delete(self):
+ '''delete the service'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create a service '''
+ return self._create_from_content(self.config.name, self.user_svc.yaml_dict)
+
+ def update(self):
+ '''create a service '''
+ # Need to copy over the portalIP and the serviceIP settings
+
+ self.user_svc.add_cluster_ip(self.service.get('spec.clusterIP'))
+ self.user_svc.add_portal_ip(self.service.get('spec.portalIP'))
+ return self._replace_content(self.kind, self.config.name, self.user_svc.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = ['clusterIP', 'portalIP']
+ return not Utils.check_def_equal(self.user_svc.yaml_dict, self.service.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''Run the idempotent ansible code'''
+ oc_svc = OCService(params['name'],
+ params['namespace'],
+ params['labels'],
+ params['selector'],
+ params['clusterip'],
+ params['portalip'],
+ params['ports'],
+ params['session_affinity'],
+ params['service_type'],
+ params['external_ips'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_svc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_svc.exists():
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'CHECK_MODE: Would have performed a delete.'} # noqa: E501
+
+ api_rval = oc_svc.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_svc.exists():
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'CHECK_MODE: Would have performed a create.'} # noqa: E501
+
+ # Create it here
+ api_rval = oc_svc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_svc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_svc.needs_update():
+ api_rval = oc_svc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_svc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'UNKNOWN state passed. [%s]' % state}
+
+# -*- -*- -*- End included fragment: class/oc_service.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_service.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for services
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, type='str'),
+ labels=dict(default=None, type='dict'),
+ selector=dict(default=None, type='dict'),
+ clusterip=dict(default=None, type='str'),
+ portalip=dict(default=None, type='str'),
+ ports=dict(default=None, type='list'),
+ session_affinity=dict(default='None', type='str'),
+ service_type=dict(default='ClusterIP', type='str'),
+ external_ips=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCService.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_service.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_serviceaccount.py b/roles/lib_openshift/library/oc_serviceaccount.py
new file mode 100644
index 000000000..646a39224
--- /dev/null
+++ b/roles/lib_openshift/library/oc_serviceaccount.py
@@ -0,0 +1,1729 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/serviceaccount -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_serviceaccount
+short_description: Module to manage openshift service accounts
+description:
+ - Manage openshift service accounts programmatically.
+options:
+ state:
+ description:
+ - If present, the service account will be created if it doesn't exist or updated if different. If absent, the service account will be removed if present. If list, information about the service account will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: false
+ aliases: []
+ name:
+ description:
+ - Name of the service account.
+ required: true
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - Namespace of the service account.
+ required: true
+ default: default
+ aliases: []
+ secrets:
+ description:
+ - A list of secrets that are associated with the service account.
+ required: false
+ default: None
+ aliases: []
+ image_pull_secrets:
+ description:
+ - A list of the image pull secrets that are associated with the service account.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create registry serviceaccount
+ oc_serviceaccount:
+ name: registry
+ namespace: default
+ secrets:
+ - docker-registry-config
+ - registry-secret
+ register: sa_out
+'''
+
+# -*- -*- -*- End included fragment: doc/serviceaccount -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/serviceaccount.py -*- -*- -*-
+
+class ServiceAccountConfig(object):
+ '''Service account config class
+
+ This class stores the options and returns a default service account
+ '''
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None):
+ self.name = sname
+ self.kubeconfig = kubeconfig
+ self.namespace = namespace
+ self.secrets = secrets or []
+ self.image_pull_secrets = image_pull_secrets or []
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiate a properly structured volume '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'ServiceAccount'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['secrets'] = []
+ if self.secrets:
+ for sec in self.secrets:
+ self.data['secrets'].append({"name": sec})
+
+ self.data['imagePullSecrets'] = []
+ if self.image_pull_secrets:
+ for sec in self.image_pull_secrets:
+ self.data['imagePullSecrets'].append({"name": sec})
+
+class ServiceAccount(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ image_pull_secrets_path = "imagePullSecrets"
+ secrets_path = "secrets"
+
+ def __init__(self, content):
+ '''ServiceAccount constructor'''
+ super(ServiceAccount, self).__init__(content=content)
+ self._secrets = None
+ self._image_pull_secrets = None
+
+ @property
+ def image_pull_secrets(self):
+ ''' property for image_pull_secrets '''
+ if self._image_pull_secrets is None:
+ self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or []
+ return self._image_pull_secrets
+
+ @image_pull_secrets.setter
+ def image_pull_secrets(self, secrets):
+ ''' property for secrets '''
+ self._image_pull_secrets = secrets
+
+ @property
+ def secrets(self):
+ ''' property for secrets '''
+ if not self._secrets:
+ self._secrets = self.get(ServiceAccount.secrets_path) or []
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self, secrets):
+ ''' property for secrets '''
+ self._secrets = secrets
+
+ def delete_secret(self, inc_secret):
+ ''' remove a secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.secrets[remove_idx]
+ return True
+
+ return False
+
+ def delete_image_pull_secret(self, inc_secret):
+ ''' remove a image_pull_secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.image_pull_secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.image_pull_secrets[remove_idx]
+ return True
+
+ return False
+
+ def find_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def find_image_pull_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.image_pull_secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def add_secret(self, inc_secret):
+ '''add secret'''
+ if self.secrets:
+ self.secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.secrets_path, [{"name": inc_secret}])
+
+ def add_image_pull_secret(self, inc_secret):
+ '''add image_pull_secret'''
+ if self.image_pull_secrets:
+ self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}])
+
+# -*- -*- -*- End included fragment: lib/serviceaccount.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_serviceaccount.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCServiceAccount(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'sa'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCServiceAccount, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self.service_account = None
+
+ def exists(self):
+ ''' return whether a volume exists '''
+ if self.service_account:
+ return True
+
+ return False
+
+ def get(self):
+ '''return volume information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.service_account = ServiceAccount(content=result['results'][0])
+ elif '\"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ # need to update the tls information and the service name
+ for secret in self.config.secrets:
+ result = self.service_account.find_secret(secret)
+ if not result:
+ self.service_account.add_secret(secret)
+
+ for secret in self.config.image_pull_secrets:
+ result = self.service_account.find_image_pull_secret(secret)
+ if not result:
+ self.service_account.add_image_pull_secret(secret)
+
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ # since creating an service account generates secrets and imagepullsecrets
+ # check_def_equal will not work
+ # Instead, verify all secrets passed are in the list
+ for secret in self.config.secrets:
+ result = self.service_account.find_secret(secret)
+ if not result:
+ return True
+
+ for secret in self.config.image_pull_secrets:
+ result = self.service_account.find_image_pull_secret(secret)
+ if not result:
+ return True
+
+ return False
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ rconfig = ServiceAccountConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['secrets'],
+ params['image_pull_secrets'],
+ )
+
+ oc_sa = OCServiceAccount(rconfig,
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_sa.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': 'list'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_sa.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_sa.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': 'absent'}
+
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_sa.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_sa.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sa.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ ########
+ # Update
+ ########
+ if oc_sa.needs_update():
+ api_rval = oc_sa.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sa.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ return {'changed': False, 'results': api_rval, 'state': 'present'}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_serviceaccount.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_serviceaccount.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for service accounts
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ secrets=dict(default=None, type='list'),
+ image_pull_secrets=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCServiceAccount.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_serviceaccount.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_serviceaccount_secret.py b/roles/lib_openshift/library/oc_serviceaccount_secret.py
new file mode 100644
index 000000000..99a8e8f3d
--- /dev/null
+++ b/roles/lib_openshift/library/oc_serviceaccount_secret.py
@@ -0,0 +1,1702 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/serviceaccount_secret -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_serviceaccount_secret
+short_description: Module to manage openshift service account secrets
+description:
+ - Manage openshift service account secrets programmatically.
+options:
+ state:
+ description:
+ - If present, the service account will be linked with the secret if it is not already. If absent, the service account will be unlinked from the secret if it is already linked. If list, information about the service account secrets will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: false
+ aliases: []
+ service_account:
+ description:
+ - Name of the service account.
+ required: true
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - Namespace of the service account and secret.
+ required: true
+ default: None
+ aliases: []
+ secret:
+ description:
+ - The secret that should be linked to the service account.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+ - name: get secrets of a service account
+ oc_serviceaccount_secret:
+ state: list
+ service_account: builder
+ namespace: default
+ register: sasecretout
+
+
+ - name: Link a service account to a specific secret
+ oc_serviceaccount_secret:
+ service_account: builder
+ secret: mynewsecret
+ namespace: default
+ register: sasecretout
+'''
+
+# -*- -*- -*- End included fragment: doc/serviceaccount_secret -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/serviceaccount.py -*- -*- -*-
+
+class ServiceAccountConfig(object):
+ '''Service account config class
+
+ This class stores the options and returns a default service account
+ '''
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None):
+ self.name = sname
+ self.kubeconfig = kubeconfig
+ self.namespace = namespace
+ self.secrets = secrets or []
+ self.image_pull_secrets = image_pull_secrets or []
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiate a properly structured volume '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'ServiceAccount'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['secrets'] = []
+ if self.secrets:
+ for sec in self.secrets:
+ self.data['secrets'].append({"name": sec})
+
+ self.data['imagePullSecrets'] = []
+ if self.image_pull_secrets:
+ for sec in self.image_pull_secrets:
+ self.data['imagePullSecrets'].append({"name": sec})
+
+class ServiceAccount(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ image_pull_secrets_path = "imagePullSecrets"
+ secrets_path = "secrets"
+
+ def __init__(self, content):
+ '''ServiceAccount constructor'''
+ super(ServiceAccount, self).__init__(content=content)
+ self._secrets = None
+ self._image_pull_secrets = None
+
+ @property
+ def image_pull_secrets(self):
+ ''' property for image_pull_secrets '''
+ if self._image_pull_secrets is None:
+ self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or []
+ return self._image_pull_secrets
+
+ @image_pull_secrets.setter
+ def image_pull_secrets(self, secrets):
+ ''' property for secrets '''
+ self._image_pull_secrets = secrets
+
+ @property
+ def secrets(self):
+ ''' property for secrets '''
+ if not self._secrets:
+ self._secrets = self.get(ServiceAccount.secrets_path) or []
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self, secrets):
+ ''' property for secrets '''
+ self._secrets = secrets
+
+ def delete_secret(self, inc_secret):
+ ''' remove a secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.secrets[remove_idx]
+ return True
+
+ return False
+
+ def delete_image_pull_secret(self, inc_secret):
+ ''' remove a image_pull_secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.image_pull_secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.image_pull_secrets[remove_idx]
+ return True
+
+ return False
+
+ def find_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def find_image_pull_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.image_pull_secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def add_secret(self, inc_secret):
+ '''add secret'''
+ if self.secrets:
+ self.secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.secrets_path, [{"name": inc_secret}])
+
+ def add_image_pull_secret(self, inc_secret):
+ '''add image_pull_secret'''
+ if self.image_pull_secrets:
+ self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}])
+
+# -*- -*- -*- End included fragment: lib/serviceaccount.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_serviceaccount_secret.py -*- -*- -*-
+
+class OCServiceAccountSecret(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ kind = 'sa'
+ def __init__(self, config, verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCServiceAccountSecret, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self.verbose = verbose
+ self._service_account = None
+
+ @property
+ def service_account(self):
+ ''' Property for the service account '''
+ if not self._service_account:
+ self.get()
+ return self._service_account
+
+ @service_account.setter
+ def service_account(self, data):
+ ''' setter for the service account '''
+ self._service_account = data
+
+ def exists(self, in_secret):
+ ''' verifies if secret exists in the service account '''
+ result = self.service_account.find_secret(in_secret)
+ if not result:
+ return False
+ return True
+
+ def get(self):
+ ''' get the service account definition from the master '''
+ sao = self._get(OCServiceAccountSecret.kind, self.config.name)
+ if sao['returncode'] == 0:
+ self.service_account = ServiceAccount(content=sao['results'][0])
+ sao['results'] = self.service_account.get('secrets')
+ return sao
+
+ def delete(self):
+ ''' delete secrets '''
+
+ modified = []
+ for rem_secret in self.config.secrets:
+ modified.append(self.service_account.delete_secret(rem_secret))
+
+ if any(modified):
+ return self._replace_content(OCServiceAccountSecret.kind, self.config.name, self.service_account.yaml_dict)
+
+ return {'returncode': 0, 'changed': False}
+
+ def put(self):
+ ''' place secrets into sa '''
+ modified = False
+ for add_secret in self.config.secrets:
+ if not self.service_account.find_secret(add_secret):
+ self.service_account.add_secret(add_secret)
+ modified = True
+
+ if modified:
+ return self._replace_content(OCServiceAccountSecret.kind, self.config.name, self.service_account.yaml_dict)
+
+ return {'returncode': 0, 'changed': False}
+
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ ''' run the ansible idempotent code '''
+
+ sconfig = ServiceAccountConfig(params['service_account'],
+ params['namespace'],
+ params['kubeconfig'],
+ [params['secret']],
+ None)
+
+ oc_sa_sec = OCServiceAccountSecret(sconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_sa_sec.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_sa_sec.exists(params['secret']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have removed the " + \
+ "secret from the service account.'}
+
+ api_rval = oc_sa_sec.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_sa_sec.exists(params['secret']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have added the ' + \
+ 'secret to the service account.'}
+
+ # Create it here
+ api_rval = oc_sa_sec.put()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sa_sec.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_serviceaccount_secret.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_serviceaccount_secret.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module to manage service account secrets.
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default=None, required=True, type='str'),
+ secret=dict(default=None, type='str'),
+ service_account=dict(required=True, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCServiceAccountSecret.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_serviceaccount_secret.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_storageclass.py b/roles/lib_openshift/library/oc_storageclass.py
new file mode 100644
index 000000000..e88f3ae8d
--- /dev/null
+++ b/roles/lib_openshift/library/oc_storageclass.py
@@ -0,0 +1,1684 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/storageclass -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_storageclass
+short_description: Create, modify, and idempotently manage openshift storageclasses.
+description:
+ - Manage openshift storageclass objects programmatically.
+options:
+ state:
+ description:
+ - State represents whether to create, modify, delete, or list
+ required: False
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ provisioner:
+ description:
+ - Any annotations to add to the storageclass
+ required: false
+ default: 'aws-ebs'
+ aliases: []
+ default_storage_class:
+ description:
+ - Whether or not this is the default storage class
+ required: false
+ default: False
+ aliases: []
+ parameters:
+ description:
+ - A dictionary with the parameters to configure the storageclass. This will be based on provisioner
+ required: false
+ default: None
+ aliases: []
+ api_version:
+ description:
+ - The api version.
+ required: false
+ default: v1
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: get storageclass
+ run_once: true
+ oc_storageclass:
+ name: gp2
+ state: list
+ register: registry_sc_out
+
+- name: create the storageclass
+ oc_storageclass:
+ run_once: true
+ name: gp2
+ parameters:
+ type: gp2
+ encrypted: 'true'
+ kmsKeyId: '<full kms key arn>'
+ provisioner: aws-ebs
+ default_storage_class: False
+ register: sc_out
+ notify:
+ - restart openshift master services
+'''
+
+# -*- -*- -*- End included fragment: doc/storageclass -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/storageclass.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class StorageClassConfig(object):
+ ''' Handle service options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ provisioner,
+ parameters=None,
+ annotations=None,
+ default_storage_class="false",
+ api_version='v1',
+ kubeconfig='/etc/origin/master/admin.kubeconfig'):
+ ''' constructor for handling storageclass options '''
+ self.name = name
+ self.parameters = parameters
+ self.annotations = annotations
+ self.provisioner = provisioner
+ self.api_version = api_version
+ self.default_storage_class = str(default_storage_class).lower()
+ self.kubeconfig = kubeconfig
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiates a storageclass dict '''
+ self.data['apiVersion'] = self.api_version
+ self.data['kind'] = 'StorageClass'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+
+ self.data['metadata']['annotations'] = {}
+ if self.annotations is not None:
+ self.data['metadata']['annotations'] = self.annotations
+
+ self.data['metadata']['annotations']['storageclass.beta.kubernetes.io/is-default-class'] = \
+ self.default_storage_class
+
+ self.data['provisioner'] = self.provisioner
+
+ self.data['parameters'] = {}
+ if self.parameters is not None:
+ self.data['parameters'].update(self.parameters)
+
+ # default to aws if no params were passed
+ else:
+ self.data['parameters']['type'] = 'gp2'
+
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class StorageClass(Yedit):
+ ''' Class to model the oc storageclass object '''
+ annotations_path = "metadata.annotations"
+ provisioner_path = "provisioner"
+ parameters_path = "parameters"
+ kind = 'StorageClass'
+
+ def __init__(self, content):
+ '''StorageClass constructor'''
+ super(StorageClass, self).__init__(content=content)
+
+ def get_annotations(self):
+ ''' get a list of ports '''
+ return self.get(StorageClass.annotations_path) or {}
+
+ def get_parameters(self):
+ ''' get the service selector'''
+ return self.get(StorageClass.parameters_path) or {}
+
+# -*- -*- -*- End included fragment: lib/storageclass.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_storageclass.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCStorageClass(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'storageclass'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCStorageClass '''
+ super(OCStorageClass, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self.storage_class = None
+
+ def exists(self):
+ ''' return whether a storageclass exists'''
+ if self.storage_class:
+ return True
+
+ return False
+
+ def get(self):
+ '''return storageclass '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.storage_class = StorageClass(content=result['results'][0])
+ elif '\"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ # parameters are currently unable to be updated. need to delete and recreate
+ self.delete()
+ # pause here and attempt to wait for delete.
+ # Better option would be to poll
+ import time
+ time.sleep(5)
+ return self.create()
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ # check if params have updated
+ if self.storage_class.get_parameters() != self.config.parameters:
+ return True
+
+ for anno_key, anno_value in self.storage_class.get_annotations().items():
+ if 'is-default-class' in anno_key and anno_value != self.config.default_storage_class:
+ return True
+
+ return False
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ rconfig = StorageClassConfig(params['name'],
+ provisioner="kubernetes.io/{}".format(params['provisioner']),
+ parameters=params['parameters'],
+ annotations=params['annotations'],
+ api_version="storage.k8s.io/{}".format(params['api_version']),
+ default_storage_class=params.get('default_storage_class', 'false'),
+ kubeconfig=params['kubeconfig'],
+ )
+
+ oc_sc = OCStorageClass(rconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_sc.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': 'list'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_sc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_sc.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': 'absent'}
+
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_sc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_sc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ ########
+ # Update
+ ########
+ if oc_sc.needs_update():
+ api_rval = oc_sc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ return {'changed': False, 'results': api_rval, 'state': 'present'}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
+
+# -*- -*- -*- End included fragment: class/oc_storageclass.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_storageclass.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for storageclass
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str', choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, type='str'),
+ annotations=dict(default=None, type='dict'),
+ parameters=dict(default=None, type='dict'),
+ provisioner=dict(required=True, type='str', choices=['aws-ebs', 'gce-pd', 'glusterfs', 'cinder']),
+ api_version=dict(default='v1', type='str'),
+ default_storage_class=dict(default="false", type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCStorageClass.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_storageclass.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_user.py b/roles/lib_openshift/library/oc_user.py
new file mode 100644
index 000000000..7bbe38819
--- /dev/null
+++ b/roles/lib_openshift/library/oc_user.py
@@ -0,0 +1,1764 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/user -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_user
+short_description: Create, modify, and idempotently manage openshift users.
+description:
+ - Modify openshift users programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update a user to the desired state
+ - 'absent' will ensure user is removed
+ - 'list' will read and return a list of users
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ username:
+ description:
+ - Short username to query/modify.
+ required: false
+ default: None
+ aliases: []
+ full_name:
+ description:
+ - String with the full name/description of the user.
+ required: false
+ default: None
+ aliases: []
+ groups:
+ description:
+ - List of groups the user should be a member of. This does not add/update the legacy 'groups' field in the OpenShift user object, but makes user entries into the appropriate OpenShift group object for the given user.
+ required: false
+ default: []
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Ensure user exists
+ oc_user:
+ state: present
+ username: johndoe
+ full_name "John Doe"
+ groups:
+ - dedicated-admins
+ register: user_johndoe
+
+user_johndoe variable will have contents like:
+ok: [ded-int-aws-master-61034] => {
+ "user_johndoe": {
+ "changed": true,
+ "results": {
+ "cmd": "oc -n default get users johndoe -o json",
+ "results": [
+ {
+ "apiVersion": "v1",
+ "fullName": "John DOe",
+ "groups": null,
+ "identities": null,
+ "kind": "User",
+ "metadata": {
+ "creationTimestamp": "2017-02-28T15:09:21Z",
+ "name": "johndoe",
+ "resourceVersion": "848781",
+ "selfLink": "/oapi/v1/users/johndoe",
+ "uid": "e23d3300-fdc7-11e6-9e3e-12822d6b7656"
+ }
+ }
+ ],
+ "returncode": 0
+ },
+ "state": "present"
+ }
+}
+'groups' is empty because this field is the OpenShift user object's 'group' field.
+
+- name: Ensure user does not exist
+ oc_user:
+ state: absent
+ username: johndoe
+
+- name: List user's info
+ oc_user:
+ state: list
+ username: johndoe
+ register: user_johndoe
+
+user_johndoe will have contents similar to:
+ok: [ded-int-aws-master-61034] => {
+ "user_johndoe": {
+ "changed": false,
+ "results": [
+ {
+ "apiVersion": "v1",
+ "fullName": "John Doe",
+ "groups": null,
+ "identities": null,
+ "kind": "User",
+ "metadata": {
+ "creationTimestamp": "2017-02-28T15:04:44Z",
+ "name": "johndoe",
+ "resourceVersion": "848280",
+ "selfLink": "/oapi/v1/users/johndoe",
+ "uid": "3d479ad2-fdc7-11e6-9e3e-12822d6b7656"
+ }
+ }
+ ],
+ "state": "list"
+ }
+}
+'''
+
+# -*- -*- -*- End included fragment: doc/user -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/user.py -*- -*- -*-
+
+
+class UserConfig(object):
+ ''' Handle user options '''
+ def __init__(self,
+ kubeconfig,
+ username,
+ full_name):
+ ''' constructor for handling user options '''
+ self.kubeconfig = kubeconfig
+ self.username = username
+ self.full_name = full_name
+
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a user as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['fullName'] = self.full_name
+ self.data['groups'] = None
+ self.data['identities'] = None
+ self.data['kind'] = 'User'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.username
+
+
+# pylint: disable=too-many-instance-attributes
+class User(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'user'
+
+ def __init__(self, content):
+ '''User constructor'''
+ super(User, self).__init__(content=content)
+
+# -*- -*- -*- End included fragment: lib/user.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_user.py -*- -*- -*-
+
+# pylint: disable=too-many-instance-attributes
+class OCUser(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'users'
+
+ def __init__(self,
+ config,
+ groups=None,
+ verbose=False):
+ ''' Constructor for OCUser '''
+ # namespace has no meaning for user operations, hardcode to 'default'
+ super(OCUser, self).__init__('default', config.kubeconfig)
+ self.config = config
+ self.groups = groups
+ self._user = None
+
+ @property
+ def user(self):
+ ''' property function user'''
+ if not self._user:
+ self.get()
+ return self._user
+
+ @user.setter
+ def user(self, data):
+ ''' setter function for user '''
+ self._user = data
+
+ def exists(self):
+ ''' return whether a user exists '''
+ if self.user:
+ return True
+
+ return False
+
+ def get(self):
+ ''' return user information '''
+ result = self._get(self.kind, self.config.username)
+ if result['returncode'] == 0:
+ self.user = User(content=result['results'][0])
+ elif 'users \"%s\" not found' % self.config.username in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ ''' delete the object '''
+ return self._delete(self.kind, self.config.username)
+
+ def create_group_entries(self):
+ ''' make entries for user to the provided group list '''
+ if self.groups != None:
+ for group in self.groups:
+ cmd = ['groups', 'add-users', group, self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+
+ return rval
+
+ return {'returncode': 0}
+
+ def create(self):
+ ''' create the object '''
+ rval = self.create_group_entries()
+ if rval['returncode'] != 0:
+ return rval
+
+ return self._create_from_content(self.config.username, self.config.data)
+
+ def group_update(self):
+ ''' update group membership '''
+ rval = {'returncode': 0}
+ cmd = ['get', 'groups', '-o', 'json']
+ all_groups = self.openshift_cmd(cmd, output=True)
+
+ # pylint misindentifying all_groups['results']['items'] type
+ # pylint: disable=invalid-sequence-index
+ for group in all_groups['results']['items']:
+ # If we're supposed to be in this group
+ if group['metadata']['name'] in self.groups \
+ and (group['users'] is None or self.config.username not in group['users']):
+ cmd = ['groups', 'add-users', group['metadata']['name'],
+ self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+ # else if we're in the group, but aren't supposed to be
+ elif group['users'] != None and self.config.username in group['users'] \
+ and group['metadata']['name'] not in self.groups:
+ cmd = ['groups', 'remove-users', group['metadata']['name'],
+ self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+
+ return rval
+
+ def update(self):
+ ''' update the object '''
+ rval = self.group_update()
+ if rval['returncode'] != 0:
+ return rval
+
+ # need to update the user's info
+ return self._replace_content(self.kind, self.config.username, self.config.data, force=True)
+
+ def needs_group_update(self):
+ ''' check if there are group membership changes '''
+ cmd = ['get', 'groups', '-o', 'json']
+ all_groups = self.openshift_cmd(cmd, output=True)
+
+ # pylint misindentifying all_groups['results']['items'] type
+ # pylint: disable=invalid-sequence-index
+ for group in all_groups['results']['items']:
+ # If we're supposed to be in this group
+ if group['metadata']['name'] in self.groups \
+ and (group['users'] is None or self.config.username not in group['users']):
+ return True
+ # else if we're in the group, but aren't supposed to be
+ elif group['users'] != None and self.config.username in group['users'] \
+ and group['metadata']['name'] not in self.groups:
+ return True
+
+ return False
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = []
+ if self.needs_group_update():
+ return True
+
+ return not Utils.check_def_equal(self.config.data, self.user.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent ansible code
+
+ params comes from the ansible portion of this module
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+
+ uconfig = UserConfig(params['kubeconfig'],
+ params['username'],
+ params['full_name'],
+ )
+
+ oc_user = OCUser(uconfig, params['groups'],
+ verbose=params['debug'])
+ state = params['state']
+
+ api_rval = oc_user.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_user.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_user.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_user.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_user.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_user.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ ########
+ # Update
+ ########
+ if oc_user.needs_update():
+ api_rval = oc_user.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ orig_cmd = api_rval['cmd']
+ # return the created object
+ api_rval = oc_user.get()
+ # overwrite the get/list cmd
+ api_rval['cmd'] = orig_cmd
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+ return {'failed': True,
+ 'changed': False,
+ 'results': 'Unknown state passed. %s' % state,
+ 'state': "unknown"}
+
+# -*- -*- -*- End included fragment: class/oc_user.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_user.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for user
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ username=dict(default=None, type='str'),
+ full_name=dict(default=None, type='str'),
+ # setting groups for user data will not populate the
+ # 'groups' field in the user data.
+ # it will call out to the group data and make the user
+ # entry there
+ groups=dict(default=[], type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = OCUser.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_user.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_version.py b/roles/lib_openshift/library/oc_version.py
new file mode 100644
index 000000000..63adbd6ac
--- /dev/null
+++ b/roles/lib_openshift/library/oc_version.py
@@ -0,0 +1,1449 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/version -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_version
+short_description: Return the current openshift version
+description:
+ - Return the openshift installed version. `oc version`
+options:
+ state:
+ description:
+ - Currently list is only supported state.
+ required: true
+ default: list
+ choices: ["list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_version:
+- name: get oc version
+ oc_version:
+ register: oc_version
+'''
+
+# -*- -*- -*- End included fragment: doc/version -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_version.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCVersion(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ debug):
+ ''' Constructor for OCVersion '''
+ super(OCVersion, self).__init__(None, config)
+ self.debug = debug
+
+ def get(self):
+ '''get and return version information '''
+
+ results = {}
+
+ version_results = self._version()
+
+ if version_results['returncode'] == 0:
+ filtered_vers = Utils.filter_versions(version_results['results'])
+ custom_vers = Utils.add_custom_versions(filtered_vers)
+
+ results['returncode'] = version_results['returncode']
+ results.update(filtered_vers)
+ results.update(custom_vers)
+
+ return results
+
+ raise OpenShiftCLIError('Problem detecting openshift version.')
+
+ @staticmethod
+ def run_ansible(params):
+ '''run the idempotent ansible code'''
+ oc_version = OCVersion(params['kubeconfig'], params['debug'])
+
+ if params['state'] == 'list':
+
+ #pylint: disable=protected-access
+ result = oc_version.get()
+ return {'state': params['state'],
+ 'results': result,
+ 'changed': False}
+
+# -*- -*- -*- End included fragment: class/oc_version.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_version.py -*- -*- -*-
+
+def main():
+ ''' ansible oc module for version '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='list', type='str',
+ choices=['list']),
+ debug=dict(default=False, type='bool'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCVersion.run_ansible(module.params)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_version.py -*- -*- -*-
diff --git a/roles/lib_openshift/library/oc_volume.py b/roles/lib_openshift/library/oc_volume.py
new file mode 100644
index 000000000..3c07f8d4b
--- /dev/null
+++ b/roles/lib_openshift/library/oc_volume.py
@@ -0,0 +1,2086 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
+
+# -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: doc/volume -*- -*- -*-
+
+DOCUMENTATION = '''
+---
+module: oc_volume
+short_description: Create, modify, and idempotently manage openshift volumes.
+description:
+ - Modify openshift volumes programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update and object to the desired state
+ - 'absent' will ensure volumes are removed
+ - 'list' will read the volumes
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ vol_name:
+ description:
+ - Name of the volume that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The name of the namespace where the object lives
+ required: false
+ default: default
+ aliases: []
+ kind:
+ description:
+ - The kind of object that can be managed.
+ default: dc
+ choices:
+ - dc
+ - rc
+ - pods
+ aliases: []
+ mount_type:
+ description:
+ - The type of volume to be used
+ required: false
+ default: None
+ choices:
+ - emptydir
+ - hostpath
+ - secret
+ - pvc
+ - configmap
+ aliases: []
+ mount_path:
+ description:
+ - The path to where the mount will be attached
+ required: false
+ default: None
+ aliases: []
+ secret_name:
+ description:
+ - The name of the secret. Used when mount_type is secret.
+ required: false
+ default: None
+ aliases: []
+ claim_size:
+ description:
+ - The size in GB of the pv claim. e.g. 100G
+ required: false
+ default: None
+ aliases: []
+ claim_name:
+ description:
+ - The name of the pv claim
+ required: false
+ default: None
+ aliases: []
+ configmap_name:
+ description:
+ - The name of the configmap
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: attach storage volumes to deploymentconfig
+ oc_volume:
+ namespace: logging
+ kind: dc
+ name: name_of_the_dc
+ mount_type: pvc
+ claim_name: loggingclaim
+ claim_size: 100G
+ vol_name: logging-storage
+ run_once: true
+'''
+
+# -*- -*- -*- End included fragment: doc/volume -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+
+class YeditException(Exception): # pragma: no cover
+ ''' Exception class for Yedit '''
+ pass
+
+
+# pylint: disable=too-many-public-methods
+class Yedit(object): # pragma: no cover
+ ''' Class to modify yaml files '''
+ re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
+ re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
+ com_sep = set(['.', '#', '|', ':'])
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ filename=None,
+ content=None,
+ content_type='yaml',
+ separator='.',
+ backup=False):
+ self.content = content
+ self._separator = separator
+ self.filename = filename
+ self.__yaml_dict = content
+ self.content_type = content_type
+ self.backup = backup
+ self.load(content_type=self.content_type)
+ if self.__yaml_dict is None:
+ self.__yaml_dict = {}
+
+ @property
+ def separator(self):
+ ''' getter method for separator '''
+ return self._separator
+
+ @separator.setter
+ def separator(self, inc_sep):
+ ''' setter method for separator '''
+ self._separator = inc_sep
+
+ @property
+ def yaml_dict(self):
+ ''' getter method for yaml_dict '''
+ return self.__yaml_dict
+
+ @yaml_dict.setter
+ def yaml_dict(self, value):
+ ''' setter method for yaml_dict '''
+ self.__yaml_dict = value
+
+ @staticmethod
+ def parse_key(key, sep='.'):
+ '''parse the key allowing the appropriate separator'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
+
+ @staticmethod
+ def valid_key(key, sep='.'):
+ '''validate the incoming key'''
+ common_separators = list(Yedit.com_sep - set([sep]))
+ if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
+ return False
+
+ return True
+
+ @staticmethod
+ def remove_entry(data, key, sep='.'):
+ ''' remove data at location key '''
+ if key == '' and isinstance(data, dict):
+ data.clear()
+ return True
+ elif key == '' and isinstance(data, list):
+ del data[:]
+ return True
+
+ if not (key and Yedit.valid_key(key, sep)) and \
+ isinstance(data, (list, dict)):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ # process last index for remove
+ # expected list entry
+ if key_indexes[-1][0]:
+ if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ del data[int(key_indexes[-1][0])]
+ return True
+
+ # expected dict entry
+ elif key_indexes[-1][1]:
+ if isinstance(data, dict):
+ del data[key_indexes[-1][1]]
+ return True
+
+ @staticmethod
+ def add_entry(data, key, item=None, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a#b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes[:-1]:
+ if dict_key:
+ if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
+ data = data[dict_key]
+ continue
+
+ elif data and not isinstance(data, dict):
+ raise YeditException("Unexpected item type found while going through key " +
+ "path: {} (at key: {})".format(key, dict_key))
+
+ data[dict_key] = {}
+ data = data[dict_key]
+
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ raise YeditException("Unexpected item type found while going through key path: {}".format(key))
+
+ if key == '':
+ data = item
+
+ # process last index for add
+ # expected list entry
+ elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
+ data[int(key_indexes[-1][0])] = item
+
+ # expected dict entry
+ elif key_indexes[-1][1] and isinstance(data, dict):
+ data[key_indexes[-1][1]] = item
+
+ # didn't add/update to an existing list, nor add/update key to a dict
+ # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
+ # non-existent array
+ else:
+ raise YeditException("Error adding to object at path: {}".format(key))
+
+ return data
+
+ @staticmethod
+ def get_entry(data, key, sep='.'):
+ ''' Get an item from a dictionary with key notation a.b.c
+ d = {'a': {'b': 'c'}}}
+ key = a.b
+ return c
+ '''
+ if key == '':
+ pass
+ elif (not (key and Yedit.valid_key(key, sep)) and
+ isinstance(data, (list, dict))):
+ return None
+
+ key_indexes = Yedit.parse_key(key, sep)
+ for arr_ind, dict_key in key_indexes:
+ if dict_key and isinstance(data, dict):
+ data = data.get(dict_key)
+ elif (arr_ind and isinstance(data, list) and
+ int(arr_ind) <= len(data) - 1):
+ data = data[int(arr_ind)]
+ else:
+ return None
+
+ return data
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ tmp_filename = filename + '.yedit'
+
+ with open(tmp_filename, 'w') as yfd:
+ yfd.write(contents)
+
+ os.rename(tmp_filename, filename)
+
+ def write(self):
+ ''' write to file '''
+ if not self.filename:
+ raise YeditException('Please specify a filename.')
+
+ if self.backup and self.file_exists():
+ shutil.copy(self.filename, self.filename + '.orig')
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripDumper if supported.
+ try:
+ Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
+ except AttributeError:
+ Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
+
+ return (True, self.yaml_dict)
+
+ def read(self):
+ ''' read from file '''
+ # check if it exists
+ if self.filename is None or not self.file_exists():
+ return None
+
+ contents = None
+ with open(self.filename) as yfd:
+ contents = yfd.read()
+
+ return contents
+
+ def file_exists(self):
+ ''' return whether file exists '''
+ if os.path.exists(self.filename):
+ return True
+
+ return False
+
+ def load(self, content_type='yaml'):
+ ''' return yaml file '''
+ contents = self.read()
+
+ if not contents and not self.content:
+ return None
+
+ if self.content:
+ if isinstance(self.content, dict):
+ self.yaml_dict = self.content
+ return self.yaml_dict
+ elif isinstance(self.content, str):
+ contents = self.content
+
+ # check if it is yaml
+ try:
+ if content_type == 'yaml' and contents:
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ # Try to use RoundTripLoader if supported.
+ try:
+ self.yaml_dict = yaml.safe_load(contents, yaml.RoundTripLoader)
+ except AttributeError:
+ self.yaml_dict = yaml.safe_load(contents)
+
+ # Try to set format attributes if supported
+ try:
+ self.yaml_dict.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ elif content_type == 'json' and contents:
+ self.yaml_dict = json.loads(contents)
+ except yaml.YAMLError as err:
+ # Error loading yaml or json
+ raise YeditException('Problem with loading yaml file. {}'.format(err))
+
+ return self.yaml_dict
+
+ def get(self, key):
+ ''' get a specified key'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
+ except KeyError:
+ entry = None
+
+ return entry
+
+ def pop(self, path, key_or_item):
+ ''' remove a key, value pair from a dict or an item for a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if key_or_item in entry:
+ entry.pop(key_or_item)
+ return (True, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ try:
+ ind = entry.index(key_or_item)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ entry.pop(ind)
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ def delete(self, path):
+ ''' remove path from a dict'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ return (False, self.yaml_dict)
+
+ result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
+ if not result:
+ return (False, self.yaml_dict)
+
+ return (True, self.yaml_dict)
+
+ def exists(self, path, value):
+ ''' check if value exists at path'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, list):
+ if value in entry:
+ return True
+ return False
+
+ elif isinstance(entry, dict):
+ if isinstance(value, dict):
+ rval = False
+ for key, val in value.items():
+ if entry[key] != val:
+ rval = False
+ break
+ else:
+ rval = True
+ return rval
+
+ return value in entry
+
+ return entry == value
+
+ def append(self, path, value):
+ '''append value to a list'''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry is None:
+ self.put(path, [])
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ if not isinstance(entry, list):
+ return (False, self.yaml_dict)
+
+ # AUDIT:maybe-no-member makes sense due to loading data from
+ # a serialized format.
+ # pylint: disable=maybe-no-member
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # pylint: disable=too-many-arguments
+ def update(self, path, value, index=None, curr_value=None):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if isinstance(entry, dict):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ if not isinstance(value, dict):
+ raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
+ 'value=[{}] type=[{}]'.format(value, type(value)))
+
+ entry.update(value)
+ return (True, self.yaml_dict)
+
+ elif isinstance(entry, list):
+ # AUDIT:maybe-no-member makes sense due to fuzzy types
+ # pylint: disable=maybe-no-member
+ ind = None
+ if curr_value:
+ try:
+ ind = entry.index(curr_value)
+ except ValueError:
+ return (False, self.yaml_dict)
+
+ elif index is not None:
+ ind = index
+
+ if ind is not None and entry[ind] != value:
+ entry[ind] = value
+ return (True, self.yaml_dict)
+
+ # see if it exists in the list
+ try:
+ ind = entry.index(value)
+ except ValueError:
+ # doesn't exist, append it
+ entry.append(value)
+ return (True, self.yaml_dict)
+
+ # already exists, return
+ if ind is not None:
+ return (False, self.yaml_dict)
+ return (False, self.yaml_dict)
+
+ def put(self, path, value):
+ ''' put path, value into a dict '''
+ try:
+ entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
+ except KeyError:
+ entry = None
+
+ if entry == value:
+ return (False, self.yaml_dict)
+
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is None:
+ return (False, self.yaml_dict)
+
+ # When path equals "" it is a special case.
+ # "" refers to the root of the document
+ # Only update the root path (entire document) when its a list or dict
+ if path == '':
+ if isinstance(result, list) or isinstance(result, dict):
+ self.yaml_dict = result
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ self.yaml_dict = tmp_copy
+
+ return (True, self.yaml_dict)
+
+ def create(self, path, value):
+ ''' create a yaml file '''
+ if not self.file_exists():
+ # deepcopy didn't work
+ # Try to use ruamel.yaml and fallback to pyyaml
+ try:
+ tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
+ default_flow_style=False),
+ yaml.RoundTripLoader)
+ except AttributeError:
+ tmp_copy = copy.deepcopy(self.yaml_dict)
+
+ # set the format attributes if available
+ try:
+ tmp_copy.fa.set_block_style()
+ except AttributeError:
+ pass
+
+ result = Yedit.add_entry(tmp_copy, path, value, self.separator)
+ if result is not None:
+ self.yaml_dict = tmp_copy
+ return (True, self.yaml_dict)
+
+ return (False, self.yaml_dict)
+
+ @staticmethod
+ def get_curr_value(invalue, val_type):
+ '''return the current value'''
+ if invalue is None:
+ return None
+
+ curr_value = invalue
+ if val_type == 'yaml':
+ curr_value = yaml.load(invalue)
+ elif val_type == 'json':
+ curr_value = json.loads(invalue)
+
+ return curr_value
+
+ @staticmethod
+ def parse_value(inc_value, vtype=''):
+ '''determine value type passed'''
+ true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON', ]
+ false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF']
+
+ # It came in as a string but you didn't specify value_type as string
+ # we will convert to bool if it matches any of the above cases
+ if isinstance(inc_value, str) and 'bool' in vtype:
+ if inc_value not in true_bools and inc_value not in false_bools:
+ raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
+ elif isinstance(inc_value, bool) and 'str' in vtype:
+ inc_value = str(inc_value)
+
+ # There is a special case where '' will turn into None after yaml loading it so skip
+ if isinstance(inc_value, str) and inc_value == '':
+ pass
+ # If vtype is not str then go ahead and attempt to yaml load it.
+ elif isinstance(inc_value, str) and 'str' not in vtype:
+ try:
+ inc_value = yaml.safe_load(inc_value)
+ except Exception:
+ raise YeditException('Could not determine type of incoming value. ' +
+ 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
+
+ return inc_value
+
+ @staticmethod
+ def process_edits(edits, yamlfile):
+ '''run through a list of edits and process them one-by-one'''
+ results = []
+ for edit in edits:
+ value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
+ if edit.get('action') == 'update':
+ # pylint: disable=line-too-long
+ curr_value = Yedit.get_curr_value(
+ Yedit.parse_value(edit.get('curr_value')),
+ edit.get('curr_value_format'))
+
+ rval = yamlfile.update(edit['key'],
+ value,
+ edit.get('index'),
+ curr_value)
+
+ elif edit.get('action') == 'append':
+ rval = yamlfile.append(edit['key'], value)
+
+ else:
+ rval = yamlfile.put(edit['key'], value)
+
+ if rval[0]:
+ results.append({'key': edit['key'], 'edit': rval[1]})
+
+ return {'changed': len(results) > 0, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params):
+ '''perform the idempotent crud operations'''
+ yamlfile = Yedit(filename=params['src'],
+ backup=params['backup'],
+ separator=params['separator'])
+
+ state = params['state']
+
+ if params['src']:
+ rval = yamlfile.load()
+
+ if yamlfile.yaml_dict is None and state != 'present':
+ return {'failed': True,
+ 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
+ 'file exists, that it is has correct permissions, and is valid yaml.'}
+
+ if state == 'list':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['key']:
+ rval = yamlfile.get(params['key'])
+
+ return {'changed': False, 'result': rval, 'state': state}
+
+ elif state == 'absent':
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+ yamlfile.yaml_dict = content
+
+ if params['update']:
+ rval = yamlfile.pop(params['key'], params['value'])
+ else:
+ rval = yamlfile.delete(params['key'])
+
+ if rval[0] and params['src']:
+ yamlfile.write()
+
+ return {'changed': rval[0], 'result': rval[1], 'state': state}
+
+ elif state == 'present':
+ # check if content is different than what is in the file
+ if params['content']:
+ content = Yedit.parse_value(params['content'], params['content_type'])
+
+ # We had no edits to make and the contents are the same
+ if yamlfile.yaml_dict == content and \
+ params['value'] is None:
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+
+ yamlfile.yaml_dict = content
+
+ # If we were passed a key, value then
+ # we enapsulate it in a list and process it
+ # Key, Value passed to the module : Converted to Edits list #
+ edits = []
+ _edit = {}
+ if params['value'] is not None:
+ _edit['value'] = params['value']
+ _edit['value_type'] = params['value_type']
+ _edit['key'] = params['key']
+
+ if params['update']:
+ _edit['action'] = 'update'
+ _edit['curr_value'] = params['curr_value']
+ _edit['curr_value_format'] = params['curr_value_format']
+ _edit['index'] = params['index']
+
+ elif params['append']:
+ _edit['action'] = 'append'
+
+ edits.append(_edit)
+
+ elif params['edits'] is not None:
+ edits = params['edits']
+
+ if edits:
+ results = Yedit.process_edits(edits, yamlfile)
+
+ # if there were changes and a src provided to us we need to write
+ if results['changed'] and params['src']:
+ yamlfile.write()
+
+ return {'changed': results['changed'], 'result': results['results'], 'state': state}
+
+ # no edits to make
+ if params['src']:
+ # pylint: disable=redefined-variable-type
+ rval = yamlfile.write()
+ return {'changed': rval[0],
+ 'result': rval[1],
+ 'state': state}
+
+ # We were passed content but no src, key or value, or edits. Return contents in memory
+ return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
+ return {'failed': True, 'msg': 'Unkown state passed'}
+
+# -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object): # pragma: no cover
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
+
+# -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
+
+# -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: lib/volume.py -*- -*- -*-
+
+class Volume(object):
+ ''' Class to represent an openshift volume object'''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ @staticmethod
+ def create_volume_structure(volume_info):
+ ''' return a properly structured volume '''
+ volume_mount = None
+ volume = {'name': volume_info['name']}
+ volume_type = volume_info['type'].lower()
+ if volume_type == 'secret':
+ volume['secret'] = {}
+ volume[volume_info['type']] = {'secretName': volume_info['secret_name']}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'emptydir':
+ volume['emptyDir'] = {}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'pvc' or volume_type == 'persistentvolumeclaim':
+ volume['persistentVolumeClaim'] = {}
+ volume['persistentVolumeClaim']['claimName'] = volume_info['claimName']
+ volume['persistentVolumeClaim']['claimSize'] = volume_info['claimSize']
+ elif volume_type == 'hostpath':
+ volume['hostPath'] = {}
+ volume['hostPath']['path'] = volume_info['path']
+ elif volume_type == 'configmap':
+ volume['configMap'] = {}
+ volume['configMap']['name'] = volume_info['configmap_name']
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+
+ return (volume, volume_mount)
+
+# -*- -*- -*- End included fragment: lib/volume.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: class/oc_volume.py -*- -*- -*-
+
+
+# pylint: disable=too-many-instance-attributes
+class OCVolume(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ resource_name,
+ namespace,
+ vol_name,
+ mount_path,
+ mount_type,
+ secret_name,
+ claim_size,
+ claim_name,
+ configmap_name,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCVolume, self).__init__(namespace, kubeconfig)
+ self.kind = kind
+ self.volume_info = {'name': vol_name,
+ 'secret_name': secret_name,
+ 'path': mount_path,
+ 'type': mount_type,
+ 'claimSize': claim_size,
+ 'claimName': claim_name,
+ 'configmap_name': configmap_name}
+ self.volume, self.volume_mount = Volume.create_volume_structure(self.volume_info)
+ self.name = resource_name
+ self.namespace = namespace
+ self.kubeconfig = kubeconfig
+ self.verbose = verbose
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var '''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var '''
+ self._resource = data
+
+ def exists(self):
+ ''' return whether a volume exists '''
+ volume_mount_found = False
+ volume_found = self.resource.exists_volume(self.volume)
+ if not self.volume_mount and volume_found:
+ return True
+
+ if self.volume_mount:
+ volume_mount_found = self.resource.exists_volume_mount(self.volume_mount)
+
+ if volume_found and self.volume_mount and volume_mount_found:
+ return True
+
+ return False
+
+ def get(self):
+ '''return volume information '''
+ vol = self._get(self.kind, self.name)
+ if vol['returncode'] == 0:
+ if self.kind == 'dc':
+ self.resource = DeploymentConfig(content=vol['results'][0])
+ vol['results'] = self.resource.get_volumes()
+
+ return vol
+
+ def delete(self):
+ '''remove a volume'''
+ self.resource.delete_volume_by_name(self.volume)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def put(self):
+ '''place volume into dc '''
+ self.resource.update_volume(self.volume)
+ self.resource.get_volumes()
+ self.resource.update_volume_mount(self.volume_mount)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ return self.resource.needs_update_volume(self.volume, self.volume_mount)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+ oc_volume = OCVolume(params['kind'],
+ params['name'],
+ params['namespace'],
+ params['vol_name'],
+ params['mount_path'],
+ params['mount_type'],
+ # secrets
+ params['secret_name'],
+ # pvc
+ params['claim_size'],
+ params['claim_name'],
+ # configmap
+ params['configmap_name'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_volume.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_volume.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_volume.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_volume.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_volume.needs_update():
+ api_rval = oc_volume.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: state}
+
+ return {'changed': False, 'results': api_rval, state: state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
+
+# -*- -*- -*- End included fragment: class/oc_volume.py -*- -*- -*-
+
+# -*- -*- -*- Begin included fragment: ansible/oc_volume.py -*- -*- -*-
+
+def main():
+ '''
+ ansible oc module for volumes
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='dc', choices=['dc', 'rc', 'pods'], type='str'),
+ namespace=dict(default='default', type='str'),
+ vol_name=dict(default=None, type='str'),
+ name=dict(default=None, type='str'),
+ mount_type=dict(default=None,
+ choices=['emptydir', 'hostpath', 'secret', 'pvc', 'configmap'],
+ type='str'),
+ mount_path=dict(default=None, type='str'),
+ # secrets require a name
+ secret_name=dict(default=None, type='str'),
+ # pvc requires a size
+ claim_size=dict(default=None, type='str'),
+ claim_name=dict(default=None, type='str'),
+ # configmap requires a name
+ configmap_name=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+ rval = OCVolume.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
+
+# -*- -*- -*- End included fragment: ansible/oc_volume.py -*- -*- -*-
diff --git a/roles/lib_openshift/meta/main.yml b/roles/lib_openshift/meta/main.yml
new file mode 100644
index 000000000..d5e7ec6e5
--- /dev/null
+++ b/roles/lib_openshift/meta/main.yml
@@ -0,0 +1,14 @@
+---
+galaxy_info:
+ author: OpenShift Team
+ description: OpenShift Repositories
+ company: Red Hat, Inc.
+ license: Apache License, Version 2.0
+ min_ansible_version: 1.7
+ platforms:
+ - name: EL
+ versions:
+ - 7
+ categories:
+ - cloud
+dependencies: []
diff --git a/roles/lib_openshift/src/ansible/oc_adm_ca_server_cert.py b/roles/lib_openshift/src/ansible/oc_adm_ca_server_cert.py
new file mode 100644
index 000000000..fc394cb43
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_ca_server_cert.py
@@ -0,0 +1,40 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=wrong-import-position
+from ansible.module_utils.six import string_types
+
+def main():
+ '''
+ ansible oc adm module for ca create-server-cert
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str', choices=['present']),
+ debug=dict(default=False, type='bool'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ backup=dict(default=True, type='bool'),
+ force=dict(default=False, type='bool'),
+ # oc adm ca create-server-cert [options]
+ cert=dict(default=None, type='str'),
+ key=dict(default=None, type='str'),
+ signer_cert=dict(default='/etc/origin/master/ca.crt', type='str'),
+ signer_key=dict(default='/etc/origin/master/ca.key', type='str'),
+ signer_serial=dict(default='/etc/origin/master/ca.serial.txt', type='str'),
+ hostnames=dict(default=[], type='list'),
+ expire_days=dict(default=None, type='int'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = CAServerCert.run_ansible(module.params, module.check_mode)
+ if 'failed' in results:
+ return module.fail_json(**results)
+
+ return module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_adm_csr.py b/roles/lib_openshift/src/ansible/oc_adm_csr.py
new file mode 100644
index 000000000..9e43a810b
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_csr.py
@@ -0,0 +1,36 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for approving certificate signing requests
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='approve', type='str',
+ choices=['approve', 'deny', 'list']),
+ debug=dict(default=False, type='bool'),
+ nodes=dict(default=None, type='list'),
+ timeout=dict(default=30, type='int'),
+ approve_all=dict(default=False, type='bool'),
+ service_account=dict(default='node-bootstrapper', type='str'),
+ ),
+ supports_check_mode=True,
+ mutually_exclusive=[['approve_all', 'nodes']],
+ )
+
+ if module.params['nodes'] == []:
+ module.fail_json(**dict(failed=True, msg='Please specify hosts.'))
+
+ rval = OCcsr.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_adm_manage_node.py b/roles/lib_openshift/src/ansible/oc_adm_manage_node.py
new file mode 100644
index 000000000..b870c1211
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_manage_node.py
@@ -0,0 +1,38 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oadm module for manage-node
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ debug=dict(default=False, type='bool'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ node=dict(default=None, type='list'),
+ selector=dict(default=None, type='str'),
+ pod_selector=dict(default=None, type='str'),
+ schedulable=dict(default=None, type='bool'),
+ list_pods=dict(default=False, type='bool'),
+ evacuate=dict(default=False, type='bool'),
+ dry_run=dict(default=False, type='bool'),
+ force=dict(default=False, type='bool'),
+ grace_period=dict(default=None, type='int'),
+ ),
+ mutually_exclusive=[["selector", "node"], ['evacuate', 'list_pods'], ['list_pods', 'schedulable']],
+ required_one_of=[["node", "selector"]],
+
+ supports_check_mode=True,
+ )
+ results = ManageNode.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_adm_policy_group.py b/roles/lib_openshift/src/ansible/oc_adm_policy_group.py
new file mode 100644
index 000000000..cf6691b03
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_policy_group.py
@@ -0,0 +1,34 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc adm module for group policy
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ resource_name=dict(required=True, type='str'),
+ namespace=dict(default='default', type='str'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+
+ group=dict(required=True, type='str'),
+ resource_kind=dict(required=True, choices=['role', 'cluster-role', 'scc'], type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = PolicyGroup.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_adm_policy_user.py b/roles/lib_openshift/src/ansible/oc_adm_policy_user.py
new file mode 100644
index 000000000..a22496866
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_policy_user.py
@@ -0,0 +1,34 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc adm module for user policy
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ resource_name=dict(required=True, type='str'),
+ namespace=dict(default='default', type='str'),
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+
+ user=dict(required=True, type='str'),
+ resource_kind=dict(required=True, choices=['role', 'cluster-role', 'scc'], type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = PolicyUser.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_adm_registry.py b/roles/lib_openshift/src/ansible/oc_adm_registry.py
new file mode 100644
index 000000000..d669a3488
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_registry.py
@@ -0,0 +1,47 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for registry
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ images=dict(default=None, type='str'),
+ latest_images=dict(default=False, type='bool'),
+ labels=dict(default=None, type='dict'),
+ ports=dict(default=['5000'], type='list'),
+ replicas=dict(default=1, type='int'),
+ selector=dict(default=None, type='str'),
+ service_account=dict(default='registry', type='str'),
+ mount_host=dict(default=None, type='str'),
+ volume_mounts=dict(default=None, type='list'),
+ env_vars=dict(default={}, type='dict'),
+ edits=dict(default=[], type='list'),
+ enforce_quota=dict(default=False, type='bool'),
+ force=dict(default=False, type='bool'),
+ daemonset=dict(default=False, type='bool'),
+ tls_key=dict(default=None, type='str'),
+ tls_certificate=dict(default=None, type='str'),
+ ),
+
+ supports_check_mode=True,
+ )
+
+ results = Registry.run_ansible(module.params, module.check_mode)
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_adm_router.py b/roles/lib_openshift/src/ansible/oc_adm_router.py
new file mode 100644
index 000000000..c6563cc2f
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_adm_router.py
@@ -0,0 +1,69 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc module for router
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', type='str',
+ choices=['present', 'absent']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default='router', type='str'),
+
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ default_cert=dict(default=None, type='str'),
+ cert_file=dict(default=None, type='str'),
+ key_file=dict(default=None, type='str'),
+ images=dict(default=None, type='str'), #'openshift3/ose-${component}:${version}'
+ latest_images=dict(default=False, type='bool'),
+ labels=dict(default=None, type='dict'),
+ ports=dict(default=['80:80', '443:443'], type='list'),
+ replicas=dict(default=1, type='int'),
+ selector=dict(default=None, type='str'),
+ service_account=dict(default='router', type='str'),
+ router_type=dict(default='haproxy-router', type='str'),
+ host_network=dict(default=True, type='bool'),
+ # external host options
+ external_host=dict(default=None, type='str'),
+ external_host_vserver=dict(default=None, type='str'),
+ external_host_insecure=dict(default=False, type='bool'),
+ external_host_partition_path=dict(default=None, type='str'),
+ external_host_username=dict(default=None, type='str'),
+ external_host_password=dict(default=None, type='str'),
+ external_host_private_key=dict(default=None, type='str'),
+ # Metrics
+ expose_metrics=dict(default=False, type='bool'),
+ metrics_image=dict(default=None, type='str'),
+ # Stats
+ stats_user=dict(default=None, type='str'),
+ stats_password=dict(default=None, type='str'),
+ stats_port=dict(default=1936, type='int'),
+ # extra
+ cacert_file=dict(default=None, type='str'),
+ # edits
+ edits=dict(default=[], type='list'),
+ ),
+ mutually_exclusive=[["router_type", "images"],
+ ["key_file", "default_cert"],
+ ["cert_file", "default_cert"],
+ ["cacert_file", "default_cert"],
+ ],
+
+ required_together=[['cacert_file', 'cert_file', 'key_file']],
+ supports_check_mode=True,
+ )
+ results = Router.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_atomic_container.py b/roles/lib_openshift/src/ansible/oc_atomic_container.py
new file mode 100644
index 000000000..454d7c4b2
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_atomic_container.py
@@ -0,0 +1,150 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=wrong-import-position,too-many-branches,invalid-name,no-name-in-module, import-error
+import json
+
+from distutils.version import StrictVersion
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def _install(module, container, image, values_list):
+ ''' install a container using atomic CLI. values_list is the list of --set arguments.
+ container is the name given to the container. image is the image to use for the installation. '''
+ # NOTE: system-package=no is hardcoded. This should be changed to an option in the future.
+ args = ['atomic', 'install', '--system', '--system-package=no',
+ '--name=%s' % container] + values_list + [image]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ return rc, out, err, False
+ else:
+ changed = "Extracting" in out or "Copying blob" in out
+ return rc, out, err, changed
+
+def _uninstall(module, name):
+ ''' uninstall an atomic container by its name. '''
+ args = ['atomic', 'uninstall', name]
+ rc, out, err = module.run_command(args, check_rc=False)
+ return rc, out, err, False
+
+
+def do_install(module, container, image, values_list):
+ ''' install a container and exit the module. '''
+ rc, out, err, changed = _install(module, container, image, values_list)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ else:
+ module.exit_json(msg=out, changed=changed)
+
+
+def do_uninstall(module, name):
+ ''' uninstall a container and exit the module. '''
+ rc, out, err, changed = _uninstall(module, name)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ module.exit_json(msg=out, changed=changed)
+
+
+def do_update(module, container, old_image, image, values_list):
+ ''' update a container and exit the module. If the container uses a different
+ image than the current installed one, then first uninstall the old one '''
+
+ # the image we want is different than the installed one
+ if old_image != image:
+ rc, out, err, _ = _uninstall(module, container)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ return do_install(module, container, image, values_list)
+
+ # if the image didn't change, use "atomic containers update"
+ args = ['atomic', 'containers', 'update'] + values_list + [container]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ else:
+ changed = "Extracting" in out or "Copying blob" in out
+ module.exit_json(msg=out, changed=changed)
+
+
+def do_rollback(module, name):
+ ''' move to the previous deployment of the container, if present, and exit the module. '''
+ args = ['atomic', 'containers', 'rollback', name]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ else:
+ changed = "Rolling back" in out
+ module.exit_json(msg=out, changed=changed)
+
+
+def core(module):
+ ''' entrypoint for the module. '''
+ name = module.params['name']
+ image = module.params['image']
+ values = module.params['values']
+ state = module.params['state']
+
+ module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
+ out = {}
+ err = {}
+ rc = 0
+
+ values_list = ["--set=%s" % x for x in values] if values else []
+
+ args = ['atomic', 'containers', 'list', '--json', '--all', '-f', 'container=%s' % name]
+ rc, out, err = module.run_command(args, check_rc=False)
+ if rc != 0:
+ module.fail_json(rc=rc, msg=err)
+ return
+
+ # NOTE: "or '[]' is a workaround until atomic containers list --json
+ # provides an empty list when no containers are present.
+ containers = json.loads(out or '[]')
+ present = len(containers) > 0
+ old_image = containers[0]["image_name"] if present else None
+
+ if state == 'present' and present:
+ module.exit_json(msg=out, changed=False)
+ elif (state in ['latest', 'present']) and not present:
+ do_install(module, name, image, values_list)
+ elif state == 'latest':
+ do_update(module, name, old_image, image, values_list)
+ elif state == 'absent':
+ if not present:
+ module.exit_json(msg="", changed=False)
+ else:
+ do_uninstall(module, name)
+ elif state == 'rollback':
+ do_rollback(module, name)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ name=dict(default=None, required=True),
+ image=dict(default=None, required=True),
+ state=dict(default='latest', choices=['present', 'absent', 'latest', 'rollback']),
+ values=dict(type='list', default=[]),
+ ),
+ )
+
+ # Verify that the platform supports atomic command
+ rc, version_out, err = module.run_command('rpm -q --queryformat "%{VERSION}\n" atomic', check_rc=False)
+ if rc != 0:
+ module.fail_json(msg="Error in running atomic command", err=err)
+ # This module requires atomic version 1.17.2 or later
+ atomic_version = StrictVersion(version_out.replace('\n', ''))
+ if atomic_version < StrictVersion('1.17.2'):
+ module.fail_json(
+ msg="atomic version 1.17.2+ is required",
+ err=str(atomic_version))
+
+ try:
+ core(module)
+ except Exception as e: # pylint: disable=broad-except
+ module.fail_json(msg=str(e))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_clusterrole.py b/roles/lib_openshift/src/ansible/oc_clusterrole.py
new file mode 100644
index 000000000..7e4319d2c
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_clusterrole.py
@@ -0,0 +1,29 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for clusterrole
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, type='str'),
+ rules=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = OCClusterRole.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_configmap.py b/roles/lib_openshift/src/ansible/oc_configmap.py
new file mode 100644
index 000000000..974f72499
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_configmap.py
@@ -0,0 +1,32 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc module for managing OpenShift configmap objects
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ from_file=dict(default=None, type='dict'),
+ from_literal=dict(default=None, type='dict'),
+ ),
+ supports_check_mode=True,
+ )
+
+
+ rval = OCConfigMap.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_edit.py b/roles/lib_openshift/src/ansible/oc_edit.py
new file mode 100644
index 000000000..5c5954747
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_edit.py
@@ -0,0 +1,48 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc module for editing objects
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ kind=dict(required=True,
+ type='str',
+ choices=['dc', 'deploymentconfig',
+ 'rc', 'replicationcontroller',
+ 'svc', 'service',
+ 'scc', 'securitycontextconstraints',
+ 'ns', 'namespace', 'project', 'projects',
+ 'is', 'imagestream',
+ 'istag', 'imagestreamtag',
+ 'bc', 'buildconfig',
+ 'routes',
+ 'node',
+ 'secret',
+ 'pv', 'persistentvolume']),
+ file_name=dict(default=None, type='str'),
+ file_format=dict(default='yaml', type='str'),
+ content=dict(default=None, required=True, type='dict'),
+ force=dict(default=False, type='bool'),
+ separator=dict(default='.', type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = Edit.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_env.py b/roles/lib_openshift/src/ansible/oc_env.py
new file mode 100644
index 000000000..e49295873
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_env.py
@@ -0,0 +1,33 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for environment variables
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='rc', choices=['dc', 'rc', 'pods'], type='str'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, required=True, type='str'),
+ env_vars=dict(default=None, type='dict'),
+ ),
+ mutually_exclusive=[["content", "files"]],
+
+ supports_check_mode=True,
+ )
+ results = OCEnv.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_group.py b/roles/lib_openshift/src/ansible/oc_group.py
new file mode 100644
index 000000000..9294286d6
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_group.py
@@ -0,0 +1,32 @@
+# pylint: skip-file
+# flake8: noqa
+
+#pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for group
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, type='str'),
+ namespace=dict(default='default', type='str'),
+ # addind users to a group is handled through the oc_users module
+ #users=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCGroup.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_image.py b/roles/lib_openshift/src/ansible/oc_image.py
new file mode 100644
index 000000000..447d62f20
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_image.py
@@ -0,0 +1,34 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc module for image import
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ registry_url=dict(default=None, type='str'),
+ image_name=dict(default=None, required=True, type='str'),
+ image_tag=dict(default=None, type='str'),
+ force=dict(default=False, type='bool'),
+ ),
+
+ supports_check_mode=True,
+ )
+
+ rval = OCImage.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_label.py b/roles/lib_openshift/src/ansible/oc_label.py
new file mode 100644
index 000000000..84c5e336a
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_label.py
@@ -0,0 +1,32 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ ''' ansible oc module for labels '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list', 'add']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='node', type='str',
+ choices=['node', 'pod', 'namespace']),
+ name=dict(default=None, type='str'),
+ namespace=dict(default=None, type='str'),
+ labels=dict(default=None, type='list'),
+ selector=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ mutually_exclusive=(['name', 'selector']),
+ )
+
+ results = OCLabel.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_obj.py b/roles/lib_openshift/src/ansible/oc_obj.py
new file mode 100644
index 000000000..6ab53d044
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_obj.py
@@ -0,0 +1,37 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for services
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ all_namespaces=dict(defaul=False, type='bool'),
+ name=dict(default=None, type='str'),
+ files=dict(default=None, type='list'),
+ kind=dict(required=True, type='str'),
+ delete_after=dict(default=False, type='bool'),
+ content=dict(default=None, type='dict'),
+ force=dict(default=False, type='bool'),
+ selector=dict(default=None, type='str'),
+ ),
+ mutually_exclusive=[["content", "files"], ["selector", "name"]],
+
+ supports_check_mode=True,
+ )
+ rval = OCObject.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_objectvalidator.py b/roles/lib_openshift/src/ansible/oc_objectvalidator.py
new file mode 100644
index 000000000..658bb5ded
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_objectvalidator.py
@@ -0,0 +1,24 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for validating OpenShift objects
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ ),
+ supports_check_mode=False,
+ )
+
+
+ rval = OCObjectValidator.run_ansible(module.params)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_process.py b/roles/lib_openshift/src/ansible/oc_process.py
new file mode 100644
index 000000000..17cf865b7
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_process.py
@@ -0,0 +1,32 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc module for processing templates
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str', choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ template_name=dict(default=None, type='str'),
+ content=dict(default=None, type='str'),
+ params=dict(default=None, type='dict'),
+ create=dict(default=False, type='bool'),
+ reconcile=dict(default=True, type='bool'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCProcess.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_project.py b/roles/lib_openshift/src/ansible/oc_project.py
new file mode 100644
index 000000000..b035cd712
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_project.py
@@ -0,0 +1,33 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for project
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, require=True, type='str'),
+ display_name=dict(default=None, type='str'),
+ node_selector=dict(default=None, type='list'),
+ description=dict(default=None, type='str'),
+ admin=dict(default=None, type='str'),
+ admin_role=dict(default='admin', type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCProject.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_pvc.py b/roles/lib_openshift/src/ansible/oc_pvc.py
new file mode 100644
index 000000000..c98d811d6
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_pvc.py
@@ -0,0 +1,35 @@
+# pylint: skip-file
+# flake8: noqa
+
+#pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for pvc
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ volume_capacity=dict(default='1G', type='str'),
+ storage_class_name=dict(default=None, required=False, type='str'),
+ selector=dict(default=None, required=False, type='dict'),
+ access_modes=dict(default=['ReadWriteOnce'], type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCPVC.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_route.py b/roles/lib_openshift/src/ansible/oc_route.py
new file mode 100644
index 000000000..969cf8bcd
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_route.py
@@ -0,0 +1,50 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-branches
+def main():
+ '''
+ ansible oc module for route
+ '''
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ labels=dict(default=None, type='dict'),
+ name=dict(default=None, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ tls_termination=dict(default=None, type='str'),
+ dest_cacert_path=dict(default=None, type='str'),
+ cacert_path=dict(default=None, type='str'),
+ cert_path=dict(default=None, type='str'),
+ key_path=dict(default=None, type='str'),
+ dest_cacert_content=dict(default=None, type='str'),
+ cacert_content=dict(default=None, type='str'),
+ cert_content=dict(default=None, type='str'),
+ key_content=dict(default=None, type='str'),
+ service_name=dict(default=None, type='str'),
+ host=dict(default=None, type='str'),
+ wildcard_policy=dict(default=None, type='str'),
+ weight=dict(default=None, type='int'),
+ port=dict(default=None, type='int'),
+ ),
+ mutually_exclusive=[('dest_cacert_path', 'dest_cacert_content'),
+ ('cacert_path', 'cacert_content'),
+ ('cert_path', 'cert_content'),
+ ('key_path', 'key_content'), ],
+ supports_check_mode=True,
+ )
+
+ results = OCRoute.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_scale.py b/roles/lib_openshift/src/ansible/oc_scale.py
new file mode 100644
index 000000000..57f9902d6
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_scale.py
@@ -0,0 +1,29 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for scaling
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str', choices=['present', 'list']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='dc', choices=['dc', 'rc'], type='str'),
+ namespace=dict(default='default', type='str'),
+ replicas=dict(default=None, type='int'),
+ name=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+ rval = OCScale.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_secret.py b/roles/lib_openshift/src/ansible/oc_secret.py
new file mode 100644
index 000000000..ee2827e69
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_secret.py
@@ -0,0 +1,39 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+def main():
+ '''
+ ansible oc module for managing OpenShift Secrets
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, type='str'),
+ annotations=dict(default=None, type='dict'),
+ type=dict(default=None, type='str'),
+ files=dict(default=None, type='list'),
+ delete_after=dict(default=False, type='bool'),
+ contents=dict(default=None, type='list'),
+ force=dict(default=False, type='bool'),
+ decode=dict(default=False, type='bool'),
+ ),
+ mutually_exclusive=[["contents", "files"]],
+
+ supports_check_mode=True,
+ )
+
+
+ rval = OCSecret.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_service.py b/roles/lib_openshift/src/ansible/oc_service.py
new file mode 100644
index 000000000..b90c08255
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_service.py
@@ -0,0 +1,37 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for services
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default='default', type='str'),
+ name=dict(default=None, type='str'),
+ labels=dict(default=None, type='dict'),
+ selector=dict(default=None, type='dict'),
+ clusterip=dict(default=None, type='str'),
+ portalip=dict(default=None, type='str'),
+ ports=dict(default=None, type='list'),
+ session_affinity=dict(default='None', type='str'),
+ service_type=dict(default='ClusterIP', type='str'),
+ external_ips=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCService.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_serviceaccount.py b/roles/lib_openshift/src/ansible/oc_serviceaccount.py
new file mode 100644
index 000000000..bc9332f9a
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_serviceaccount.py
@@ -0,0 +1,30 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for service accounts
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, required=True, type='str'),
+ namespace=dict(default=None, required=True, type='str'),
+ secrets=dict(default=None, type='list'),
+ image_pull_secrets=dict(default=None, type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCServiceAccount.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_serviceaccount_secret.py b/roles/lib_openshift/src/ansible/oc_serviceaccount_secret.py
new file mode 100644
index 000000000..554d9a6a2
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_serviceaccount_secret.py
@@ -0,0 +1,29 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module to manage service account secrets.
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ namespace=dict(default=None, required=True, type='str'),
+ secret=dict(default=None, type='str'),
+ service_account=dict(required=True, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCServiceAccountSecret.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_storageclass.py b/roles/lib_openshift/src/ansible/oc_storageclass.py
new file mode 100644
index 000000000..e9f3ebbd3
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_storageclass.py
@@ -0,0 +1,32 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for storageclass
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str', choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ name=dict(default=None, type='str'),
+ annotations=dict(default=None, type='dict'),
+ parameters=dict(default=None, type='dict'),
+ provisioner=dict(required=True, type='str', choices=['aws-ebs', 'gce-pd', 'glusterfs', 'cinder']),
+ api_version=dict(default='v1', type='str'),
+ default_storage_class=dict(default="false", type='str'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCStorageClass.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ return module.fail_json(**rval)
+
+ return module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_user.py b/roles/lib_openshift/src/ansible/oc_user.py
new file mode 100644
index 000000000..6b1440796
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_user.py
@@ -0,0 +1,34 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for user
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ username=dict(default=None, type='str'),
+ full_name=dict(default=None, type='str'),
+ # setting groups for user data will not populate the
+ # 'groups' field in the user data.
+ # it will call out to the group data and make the user
+ # entry there
+ groups=dict(default=[], type='list'),
+ ),
+ supports_check_mode=True,
+ )
+
+ results = OCUser.run_ansible(module.params, module.check_mode)
+
+ if 'failed' in results:
+ module.fail_json(**results)
+
+ module.exit_json(**results)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_version.py b/roles/lib_openshift/src/ansible/oc_version.py
new file mode 100644
index 000000000..57ef849ca
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_version.py
@@ -0,0 +1,26 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ ''' ansible oc module for version '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='list', type='str',
+ choices=['list']),
+ debug=dict(default=False, type='bool'),
+ ),
+ supports_check_mode=True,
+ )
+
+ rval = OCVersion.run_ansible(module.params)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/ansible/oc_volume.py b/roles/lib_openshift/src/ansible/oc_volume.py
new file mode 100644
index 000000000..660376d2f
--- /dev/null
+++ b/roles/lib_openshift/src/ansible/oc_volume.py
@@ -0,0 +1,41 @@
+# pylint: skip-file
+# flake8: noqa
+
+def main():
+ '''
+ ansible oc module for volumes
+ '''
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
+ state=dict(default='present', type='str',
+ choices=['present', 'absent', 'list']),
+ debug=dict(default=False, type='bool'),
+ kind=dict(default='dc', choices=['dc', 'rc', 'pods'], type='str'),
+ namespace=dict(default='default', type='str'),
+ vol_name=dict(default=None, type='str'),
+ name=dict(default=None, type='str'),
+ mount_type=dict(default=None,
+ choices=['emptydir', 'hostpath', 'secret', 'pvc', 'configmap'],
+ type='str'),
+ mount_path=dict(default=None, type='str'),
+ # secrets require a name
+ secret_name=dict(default=None, type='str'),
+ # pvc requires a size
+ claim_size=dict(default=None, type='str'),
+ claim_name=dict(default=None, type='str'),
+ # configmap requires a name
+ configmap_name=dict(default=None, type='str'),
+ ),
+ supports_check_mode=True,
+ )
+ rval = OCVolume.run_ansible(module.params, module.check_mode)
+ if 'failed' in rval:
+ module.fail_json(**rval)
+
+ module.exit_json(**rval)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/class/oc_adm_ca_server_cert.py b/roles/lib_openshift/src/class/oc_adm_ca_server_cert.py
new file mode 100644
index 000000000..37a64e4ef
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_ca_server_cert.py
@@ -0,0 +1,146 @@
+# pylint: skip-file
+# flake8: noqa
+
+class CAServerCertConfig(OpenShiftCLIConfig):
+ ''' CAServerCertConfig is a DTO for the oc adm ca command '''
+ def __init__(self, kubeconfig, verbose, ca_options):
+ super(CAServerCertConfig, self).__init__('ca', None, kubeconfig, ca_options)
+ self.kubeconfig = kubeconfig
+ self.verbose = verbose
+ self._ca = ca_options
+
+
+class CAServerCert(OpenShiftCLI):
+ ''' Class to wrap the oc adm ca create-server-cert command line'''
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for oadm ca '''
+ super(CAServerCert, self).__init__(None, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+
+ def get(self):
+ '''get the current cert file
+
+ If a file exists by the same name in the specified location then the cert exists
+ '''
+ cert = self.config.config_options['cert']['value']
+ if cert and os.path.exists(cert):
+ return open(cert).read()
+
+ return None
+
+ def create(self):
+ '''run openshift oc adm ca create-server-cert cmd'''
+
+ # Added this here as a safegaurd for stomping on the
+ # cert and key files if they exist
+ if self.config.config_options['backup']['value']:
+ import time
+ ext = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
+ date_str = "%s_" + "%s" % ext
+
+ if os.path.exists(self.config.config_options['key']['value']):
+ shutil.copy(self.config.config_options['key']['value'],
+ date_str % self.config.config_options['key']['value'])
+ if os.path.exists(self.config.config_options['cert']['value']):
+ shutil.copy(self.config.config_options['cert']['value'],
+ date_str % self.config.config_options['cert']['value'])
+
+ options = self.config.to_option_list()
+
+ cmd = ['ca', 'create-server-cert']
+ cmd.extend(options)
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ def exists(self):
+ ''' check whether the certificate exists and has the clusterIP '''
+
+ cert_path = self.config.config_options['cert']['value']
+ if not os.path.exists(cert_path):
+ return False
+
+ # Would prefer pyopenssl but is not installed.
+ # When we verify it is, switch this code
+ # Here is the code to get the subject and the SAN
+ # openssl x509 -text -noout -certopt \
+ # no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux \
+ # -in /etc/origin/master/registry.crt
+ # Instead of this solution we will use a regex.
+ cert_names = []
+ hostnames = self.config.config_options['hostnames']['value'].split(',')
+ proc = subprocess.Popen(['openssl', 'x509', '-noout', '-text', '-in', cert_path],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ x509output, _ = proc.communicate()
+ if proc.returncode == 0:
+ regex = re.compile(r"^\s*X509v3 Subject Alternative Name:\s*?\n\s*(.*)\s*\n", re.MULTILINE)
+ match = regex.search(x509output.decode()) # E501
+ if not match:
+ return False
+
+ for entry in re.split(r", *", match.group(1)):
+ if entry.startswith('DNS') or entry.startswith('IP Address'):
+ cert_names.append(entry.split(':')[1])
+ # now that we have cert names let's compare
+ cert_set = set(cert_names)
+ hname_set = set(hostnames)
+ if cert_set.issubset(hname_set) and hname_set.issubset(cert_set):
+ return True
+
+ return False
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ # Filter non-strings from hostnames list s.t. the omit filter
+ # may be used to conditionally add a hostname.
+ params['hostnames'] = [host for host in params['hostnames'] if isinstance(host, string_types)]
+
+ config = CAServerCertConfig(params['kubeconfig'],
+ params['debug'],
+ {'cert': {'value': params['cert'], 'include': True},
+ 'hostnames': {'value': ','.join(params['hostnames']), 'include': True},
+ 'overwrite': {'value': True, 'include': True},
+ 'key': {'value': params['key'], 'include': True},
+ 'signer_cert': {'value': params['signer_cert'], 'include': True},
+ 'signer_key': {'value': params['signer_key'], 'include': True},
+ 'signer_serial': {'value': params['signer_serial'], 'include': True},
+ 'expire_days': {'value': params['expire_days'], 'include': True},
+ 'backup': {'value': params['backup'], 'include': False},
+ })
+
+ server_cert = CAServerCert(config)
+
+ state = params['state']
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not server_cert.exists() or params['force']:
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': "CHECK_MODE: Would have created the certificate.",
+ 'state': state}
+
+ api_rval = server_cert.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Exists
+ ########
+ api_rval = server_cert.get()
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'msg': 'Unknown state passed. %s' % state}
+
diff --git a/roles/lib_openshift/src/class/oc_adm_csr.py b/roles/lib_openshift/src/class/oc_adm_csr.py
new file mode 100644
index 000000000..ea11c6ca9
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_csr.py
@@ -0,0 +1,197 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class OCcsr(OpenShiftCLI):
+ ''' Class to wrap the oc adm certificate command line'''
+ kind = 'csr'
+
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ nodes=None,
+ approve_all=False,
+ service_account=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for oc adm certificate '''
+ super(OCcsr, self).__init__(None, kubeconfig, verbose)
+ self.service_account = service_account
+ self.nodes = self.create_nodes(nodes)
+ self._csrs = []
+ self.approve_all = approve_all
+ self.verbose = verbose
+
+ @property
+ def csrs(self):
+ '''property for managing csrs'''
+ # any processing needed??
+ self._csrs = self._get(resource=self.kind)['results'][0]['items']
+ return self._csrs
+
+ def create_nodes(self, nodes):
+ '''create a node object to track csr signing status'''
+ nodes_list = []
+
+ if nodes is None:
+ return nodes_list
+
+ results = self._get(resource='nodes')['results'][0]['items']
+
+ for node in nodes:
+ nodes_list.append(dict(name=node, csrs={}, accepted=False, denied=False))
+
+ for ocnode in results:
+ if node in ocnode['metadata']['name']:
+ nodes_list[-1]['accepted'] = True
+
+ return nodes_list
+
+ def get(self):
+ '''get the current certificate signing requests'''
+ return self.csrs
+
+ @staticmethod
+ def action_needed(csr, action):
+ '''check to see if csr is in desired state'''
+ if csr['status'] == {}:
+ return True
+
+ state = csr['status']['conditions'][0]['type']
+
+ if action == 'approve' and state != 'Approved':
+ return True
+
+ elif action == 'deny' and state != 'Denied':
+ return True
+
+ return False
+
+ def match_node(self, csr):
+ '''match an inc csr to a node in self.nodes'''
+ for node in self.nodes:
+ # we have a match
+ if node['name'] in csr['metadata']['name']:
+ node['csrs'][csr['metadata']['name']] = csr
+
+ # check that the username is the node and type is 'Approved'
+ if node['name'] in csr['spec']['username'] and csr['status']:
+ if csr['status']['conditions'][0]['type'] == 'Approved':
+ node['accepted'] = True
+ # check type is 'Denied' and mark node as such
+ if csr['status'] and csr['status']['conditions'][0]['type'] == 'Denied':
+ node['denied'] = True
+
+ return node
+
+ return None
+
+ def finished(self):
+ '''determine if there are more csrs to sign'''
+ # if nodes is set and we have nodes then return if all nodes are 'accepted'
+ if self.nodes is not None and len(self.nodes) > 0:
+ return all([node['accepted'] or node['denied'] for node in self.nodes])
+
+ # we are approving everything or we still have nodes outstanding
+ return False
+
+ def manage(self, action):
+ '''run openshift oc adm ca create-server-cert cmd and store results into self.nodes
+
+ we attempt to verify if the node is one that was given to us to accept.
+
+ action - (allow | deny)
+ '''
+
+ results = []
+ # There are 2 types of requests:
+ # - node-bootstrapper-client-ip-172-31-51-246-ec2-internal
+ # The client request allows the client to talk to the api/controller
+ # - node-bootstrapper-server-ip-172-31-51-246-ec2-internal
+ # The server request allows the server to join the cluster
+ # Here we need to determine how to approve/deny
+ # we should query the csrs and verify they are from the nodes we thought
+ for csr in self.csrs:
+ node = self.match_node(csr)
+ # oc adm certificate <approve|deny> csr
+ # there are 3 known states: Denied, Aprroved, {}
+ # verify something is needed by OCcsr.action_needed
+ # if approve_all, then do it
+ # if you passed in nodes, you must have a node that matches
+ if self.approve_all or (node and OCcsr.action_needed(csr, action)):
+ result = self.openshift_cmd(['certificate', action, csr['metadata']['name']], oadm=True)
+ # client should have service account name in username field
+ # server should have node name in username field
+ if node and csr['metadata']['name'] not in node['csrs']:
+ node['csrs'][csr['metadata']['name']] = csr
+
+ # accept node in cluster
+ if node['name'] in csr['spec']['username']:
+ node['accepted'] = True
+
+ results.append(result)
+
+ return results
+
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+
+ client = OCcsr(params['nodes'],
+ params['approve_all'],
+ params['service_account'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = client.get()
+
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if state in ['approve', 'deny']:
+ if check_mode:
+ return {'changed': True,
+ 'msg': "CHECK_MODE: Would have {} the certificate.".format(params['state']),
+ 'state': state}
+
+ all_results = []
+ finished = False
+ timeout = False
+ import time
+ # loop for timeout or block until all nodes pass
+ ctr = 0
+ while True:
+
+ all_results.extend(client.manage(params['state']))
+ if client.finished():
+ finished = True
+ break
+
+ if params['timeout'] == 0:
+ if not params['approve_all']:
+ ctr = 0
+
+ if ctr * 2 > params['timeout']:
+ timeout = True
+ break
+
+ # This provides time for the nodes to send their csr requests between approvals
+ time.sleep(2)
+
+ ctr += 1
+
+ for result in all_results:
+ if result['returncode'] != 0:
+ return {'failed': True, 'msg': all_results}
+
+ return dict(changed=len(all_results) > 0,
+ results=all_results,
+ nodes=client.nodes,
+ state=state,
+ finished=finished,
+ timeout=timeout)
+
+ return {'failed': True,
+ 'msg': 'Unknown state passed. %s' % state}
+
diff --git a/roles/lib_openshift/src/class/oc_adm_manage_node.py b/roles/lib_openshift/src/class/oc_adm_manage_node.py
new file mode 100644
index 000000000..6d9f24baa
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_manage_node.py
@@ -0,0 +1,209 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class ManageNodeException(Exception):
+ ''' manage-node exception class '''
+ pass
+
+
+class ManageNodeConfig(OpenShiftCLIConfig):
+ ''' ManageNodeConfig is a DTO for the manage-node command.'''
+ def __init__(self, kubeconfig, node_options):
+ super(ManageNodeConfig, self).__init__(None, None, kubeconfig, node_options)
+
+
+# pylint: disable=too-many-instance-attributes
+class ManageNode(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for ManageNode '''
+ super(ManageNode, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+
+ def evacuate(self):
+ ''' formulate the params and run oadm manage-node '''
+ return self._evacuate(node=self.config.config_options['node']['value'],
+ selector=self.config.config_options['selector']['value'],
+ pod_selector=self.config.config_options['pod_selector']['value'],
+ dry_run=self.config.config_options['dry_run']['value'],
+ grace_period=self.config.config_options['grace_period']['value'],
+ force=self.config.config_options['force']['value'],
+ )
+ def get_nodes(self, node=None, selector=''):
+ '''perform oc get node'''
+ _node = None
+ _sel = None
+ if node:
+ _node = node
+ if selector:
+ _sel = selector
+
+ results = self._get('node', name=_node, selector=_sel)
+ if results['returncode'] != 0:
+ return results
+
+ nodes = []
+ items = None
+ if results['results'][0]['kind'] == 'List':
+ items = results['results'][0]['items']
+ else:
+ items = results['results']
+
+ for node in items:
+ _node = {}
+ _node['name'] = node['metadata']['name']
+ _node['schedulable'] = True
+ if 'unschedulable' in node['spec']:
+ _node['schedulable'] = False
+ nodes.append(_node)
+
+ return nodes
+
+ def get_pods_from_node(self, node, pod_selector=None):
+ '''return pods for a node'''
+ results = self._list_pods(node=[node], pod_selector=pod_selector)
+
+ if results['returncode'] != 0:
+ return results
+
+ # When a selector or node is matched it is returned along with the json.
+ # We are going to split the results based on the regexp and then
+ # load the json for each matching node.
+ # Before we return we are going to loop over the results and pull out the node names.
+ # {'node': [pod, pod], 'node': [pod, pod]}
+ # 3.2 includes the following lines in stdout: "Listing matched pods on node:"
+ all_pods = []
+ if "Listing matched" in results['results']:
+ listing_match = re.compile('\n^Listing matched.*$\n', flags=re.MULTILINE)
+ pods = listing_match.split(results['results'])
+ for pod in pods:
+ if pod:
+ all_pods.extend(json.loads(pod)['items'])
+
+ # 3.3 specific
+ else:
+ # this is gross but I filed a bug...
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1381621
+ # build our own json from the output.
+ all_pods = json.loads(results['results'])['items']
+
+ return all_pods
+
+ def list_pods(self):
+ ''' run oadm manage-node --list-pods'''
+ _nodes = self.config.config_options['node']['value']
+ _selector = self.config.config_options['selector']['value']
+ _pod_selector = self.config.config_options['pod_selector']['value']
+
+ if not _nodes:
+ _nodes = self.get_nodes(selector=_selector)
+ else:
+ _nodes = [{'name': name} for name in _nodes]
+
+ all_pods = {}
+ for node in _nodes:
+ results = self.get_pods_from_node(node['name'], pod_selector=_pod_selector)
+ if isinstance(results, dict):
+ return results
+ all_pods[node['name']] = results
+
+ results = {}
+ results['nodes'] = all_pods
+ results['returncode'] = 0
+ return results
+
+ def schedulable(self):
+ '''oadm manage-node call for making nodes unschedulable'''
+ nodes = self.config.config_options['node']['value']
+ selector = self.config.config_options['selector']['value']
+
+ if not nodes:
+ nodes = self.get_nodes(selector=selector)
+ else:
+ tmp_nodes = []
+ for name in nodes:
+ tmp_result = self.get_nodes(name)
+ if isinstance(tmp_result, dict):
+ tmp_nodes.append(tmp_result)
+ continue
+ tmp_nodes.extend(tmp_result)
+ nodes = tmp_nodes
+
+ # This is a short circuit based on the way we fetch nodes.
+ # If node is a dict/list then we've already fetched them.
+ for node in nodes:
+ if isinstance(node, dict) and 'returncode' in node:
+ return {'results': nodes, 'returncode': node['returncode']}
+ if isinstance(node, list) and 'returncode' in node[0]:
+ return {'results': nodes, 'returncode': node[0]['returncode']}
+ # check all the nodes that were returned and verify they are:
+ # node['schedulable'] == self.config.config_options['schedulable']['value']
+ if any([node['schedulable'] != self.config.config_options['schedulable']['value'] for node in nodes]):
+
+ results = self._schedulable(node=self.config.config_options['node']['value'],
+ selector=self.config.config_options['selector']['value'],
+ schedulable=self.config.config_options['schedulable']['value'])
+
+ # 'NAME STATUS AGE\\nip-172-31-49-140.ec2.internal Ready 4h\\n' # E501
+ # normalize formatting with previous return objects
+ if results['results'].startswith('NAME'):
+ nodes = []
+ # removing header line and trailing new line character of node lines
+ for node_results in results['results'].split('\n')[1:-1]:
+ parts = node_results.split()
+ nodes.append({'name': parts[0], 'schedulable': parts[1] == 'Ready'})
+ results['nodes'] = nodes
+
+ return results
+
+ results = {}
+ results['returncode'] = 0
+ results['changed'] = False
+ results['nodes'] = nodes
+
+ return results
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+ nconfig = ManageNodeConfig(params['kubeconfig'],
+ {'node': {'value': params['node'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'pod_selector': {'value': params['pod_selector'], 'include': True},
+ 'schedulable': {'value': params['schedulable'], 'include': True},
+ 'list_pods': {'value': params['list_pods'], 'include': True},
+ 'evacuate': {'value': params['evacuate'], 'include': True},
+ 'dry_run': {'value': params['dry_run'], 'include': True},
+ 'force': {'value': params['force'], 'include': True},
+ 'grace_period': {'value': params['grace_period'], 'include': True},
+ })
+
+ oadm_mn = ManageNode(nconfig)
+ # Run the oadm manage-node commands
+ results = None
+ changed = False
+ if params['schedulable'] != None:
+ if check_mode:
+ # schedulable returns results after the fact.
+ # We need to redo how this works to support check_mode completely.
+ return {'changed': True, 'msg': 'CHECK_MODE: would have called schedulable.'}
+ results = oadm_mn.schedulable()
+ if 'changed' not in results:
+ changed = True
+
+ if params['evacuate']:
+ results = oadm_mn.evacuate()
+ changed = True
+ elif params['list_pods']:
+ results = oadm_mn.list_pods()
+
+ if not results or results['returncode'] != 0:
+ return {'failed': True, 'msg': results}
+
+ return {'changed': changed, 'results': results, 'state': "present"}
diff --git a/roles/lib_openshift/src/class/oc_adm_policy_group.py b/roles/lib_openshift/src/class/oc_adm_policy_group.py
new file mode 100644
index 000000000..6ad57bdce
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_policy_group.py
@@ -0,0 +1,222 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class PolicyGroupException(Exception):
+ ''' PolicyGroup exception'''
+ pass
+
+
+class PolicyGroupConfig(OpenShiftCLIConfig):
+ ''' PolicyGroupConfig is a DTO for group related policy. '''
+ def __init__(self, namespace, kubeconfig, policy_options):
+ super(PolicyGroupConfig, self).__init__(policy_options['name']['value'],
+ namespace, kubeconfig, policy_options)
+ self.kind = self.get_kind()
+ self.namespace = namespace
+
+ def get_kind(self):
+ ''' return the kind we are working with '''
+ if self.config_options['resource_kind']['value'] == 'role':
+ return 'rolebinding'
+ elif self.config_options['resource_kind']['value'] == 'cluster-role':
+ return 'clusterrolebinding'
+ elif self.config_options['resource_kind']['value'] == 'scc':
+ return 'scc'
+
+ return None
+
+
+# pylint: disable=too-many-return-statements
+class PolicyGroup(OpenShiftCLI):
+ ''' Class to handle attaching policies to users '''
+
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for PolicyGroup '''
+ super(PolicyGroup, self).__init__(config.namespace, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+ self._rolebinding = None
+ self._scc = None
+ self._cluster_role_bindings = None
+ self._role_bindings = None
+
+ @property
+ def rolebindings(self):
+ if self._role_bindings is None:
+ results = self._get('rolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve rolebindings')
+ self._role_bindings = results['results'][0]['items']
+
+ return self._role_bindings
+
+ @property
+ def clusterrolebindings(self):
+ if self._cluster_role_bindings is None:
+ results = self._get('clusterrolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve clusterrolebindings')
+ self._cluster_role_bindings = results['results'][0]['items']
+
+ return self._cluster_role_bindings
+
+ @property
+ def role_binding(self):
+ ''' role_binding getter '''
+ return self._rolebinding
+
+ @role_binding.setter
+ def role_binding(self, binding):
+ ''' role_binding setter '''
+ self._rolebinding = binding
+
+ @property
+ def security_context_constraint(self):
+ ''' security_context_constraint getter '''
+ return self._scc
+
+ @security_context_constraint.setter
+ def security_context_constraint(self, scc):
+ ''' security_context_constraint setter '''
+ self._scc = scc
+
+ def get(self):
+ '''fetch the desired kind'''
+ resource_name = self.config.config_options['name']['value']
+ if resource_name == 'cluster-reader':
+ resource_name += 's'
+
+ # oc adm policy add-... creates policy bindings with the name
+ # "[resource_name]-binding", however some bindings in the system
+ # simply use "[resource_name]". So try both.
+
+ results = self._get(self.config.kind, resource_name)
+ if results['returncode'] == 0:
+ return results
+
+ # Now try -binding naming convention
+ return self._get(self.config.kind, resource_name + "-binding")
+
+ def exists_role_binding(self):
+ ''' return whether role_binding exists '''
+ bindings = None
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ bindings = self.clusterrolebindings
+ else:
+ bindings = self.rolebindings
+
+ if bindings is None:
+ return False
+
+ for binding in bindings:
+ if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \
+ binding['groupNames'] is not None and \
+ self.config.config_options['group']['value'] in binding['groupNames']:
+ self.role_binding = binding
+ return True
+
+ return False
+
+ def exists_scc(self):
+ ''' return whether scc exists '''
+ results = self.get()
+ if results['returncode'] == 0:
+ self.security_context_constraint = SecurityContextConstraints(results['results'][0])
+
+ if self.security_context_constraint.find_group(self.config.config_options['group']['value']) != None:
+ return True
+
+ return False
+
+ return results
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'scc':
+ return self.exists_scc()
+
+ return False
+
+ def perform(self):
+ '''perform action on resource'''
+ cmd = ['policy',
+ self.config.config_options['action']['value'],
+ self.config.config_options['name']['value'],
+ self.config.config_options['group']['value']]
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ state = params['state']
+
+ action = None
+ if state == 'present':
+ action = 'add-' + params['resource_kind'] + '-to-group'
+ else:
+ action = 'remove-' + params['resource_kind'] + '-from-group'
+
+ nconfig = PolicyGroupConfig(params['namespace'],
+ params['kubeconfig'],
+ {'action': {'value': action, 'include': False},
+ 'group': {'value': params['group'], 'include': False},
+ 'resource_kind': {'value': params['resource_kind'], 'include': False},
+ 'name': {'value': params['resource_name'], 'include': False},
+ })
+
+ policygroup = PolicyGroup(nconfig, params['debug'])
+
+ # Run the oc adm policy group related command
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not policygroup.exists():
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a delete.'}
+
+ api_rval = policygroup.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results' : api_rval, state:'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ results = policygroup.exists()
+ if isinstance(results, dict) and 'returncode' in results and results['returncode'] != 0:
+ return {'msg': results}
+
+ if not results:
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a create.'}
+
+ api_rval = policygroup.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: 'present'}
+
+ return {'changed': False, state: 'present'}
+
+ return {'failed': True, 'changed': False, 'results': 'Unknown state passed. %s' % state, state: 'unknown'}
diff --git a/roles/lib_openshift/src/class/oc_adm_policy_user.py b/roles/lib_openshift/src/class/oc_adm_policy_user.py
new file mode 100644
index 000000000..6fc8145c8
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_policy_user.py
@@ -0,0 +1,216 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class PolicyUserException(Exception):
+ ''' PolicyUser exception'''
+ pass
+
+
+class PolicyUserConfig(OpenShiftCLIConfig):
+ ''' PolicyUserConfig is a DTO for user related policy. '''
+ def __init__(self, namespace, kubeconfig, policy_options):
+ super(PolicyUserConfig, self).__init__(policy_options['name']['value'],
+ namespace, kubeconfig, policy_options)
+ self.kind = self.get_kind()
+ self.namespace = namespace
+
+ def get_kind(self):
+ ''' return the kind we are working with '''
+ if self.config_options['resource_kind']['value'] == 'role':
+ return 'rolebinding'
+ elif self.config_options['resource_kind']['value'] == 'cluster-role':
+ return 'clusterrolebinding'
+ elif self.config_options['resource_kind']['value'] == 'scc':
+ return 'scc'
+
+ return None
+
+
+# pylint: disable=too-many-return-statements
+class PolicyUser(OpenShiftCLI):
+ ''' Class to handle attaching policies to users '''
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for PolicyUser '''
+ super(PolicyUser, self).__init__(config.namespace, config.kubeconfig, verbose)
+ self.config = config
+ self.verbose = verbose
+ self._rolebinding = None
+ self._scc = None
+ self._cluster_role_bindings = None
+ self._role_bindings = None
+
+ @property
+ def rolebindings(self):
+ if self._role_bindings is None:
+ results = self._get('rolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve rolebindings')
+ self._role_bindings = results['results'][0]['items']
+
+ return self._role_bindings
+
+ @property
+ def clusterrolebindings(self):
+ if self._cluster_role_bindings is None:
+ results = self._get('clusterrolebindings', None)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Could not retrieve clusterrolebindings')
+ self._cluster_role_bindings = results['results'][0]['items']
+
+ return self._cluster_role_bindings
+
+ @property
+ def role_binding(self):
+ ''' role_binding property '''
+ return self._rolebinding
+
+ @role_binding.setter
+ def role_binding(self, binding):
+ ''' setter for role_binding property '''
+ self._rolebinding = binding
+
+ @property
+ def security_context_constraint(self):
+ ''' security_context_constraint property '''
+ return self._scc
+
+ @security_context_constraint.setter
+ def security_context_constraint(self, scc):
+ ''' setter for security_context_constraint property '''
+ self._scc = scc
+
+ def get(self):
+ '''fetch the desired kind
+
+ This is only used for scc objects.
+ The {cluster}rolebindings happen in exists.
+ '''
+ resource_name = self.config.config_options['name']['value']
+ if resource_name == 'cluster-reader':
+ resource_name += 's'
+
+ return self._get(self.config.kind, resource_name)
+
+ def exists_role_binding(self):
+ ''' return whether role_binding exists '''
+ bindings = None
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ bindings = self.clusterrolebindings
+ else:
+ bindings = self.rolebindings
+
+ if bindings is None:
+ return False
+
+ for binding in bindings:
+ if binding['roleRef']['name'] == self.config.config_options['name']['value'] and \
+ binding['userNames'] is not None and \
+ self.config.config_options['user']['value'] in binding['userNames']:
+ self.role_binding = binding
+ return True
+
+ return False
+
+ def exists_scc(self):
+ ''' return whether scc exists '''
+ results = self.get()
+ if results['returncode'] == 0:
+ self.security_context_constraint = SecurityContextConstraints(results['results'][0])
+
+ if self.security_context_constraint.find_user(self.config.config_options['user']['value']) != None:
+ return True
+
+ return False
+
+ return results
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.config.config_options['resource_kind']['value'] == 'cluster-role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'role':
+ return self.exists_role_binding()
+
+ elif self.config.config_options['resource_kind']['value'] == 'scc':
+ return self.exists_scc()
+
+ return False
+
+ def perform(self):
+ '''perform action on resource'''
+ cmd = ['policy',
+ self.config.config_options['action']['value'],
+ self.config.config_options['name']['value'],
+ self.config.config_options['user']['value']]
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ state = params['state']
+
+ action = None
+ if state == 'present':
+ action = 'add-' + params['resource_kind'] + '-to-user'
+ else:
+ action = 'remove-' + params['resource_kind'] + '-from-user'
+
+ nconfig = PolicyUserConfig(params['namespace'],
+ params['kubeconfig'],
+ {'action': {'value': action, 'include': False},
+ 'user': {'value': params['user'], 'include': False},
+ 'resource_kind': {'value': params['resource_kind'], 'include': False},
+ 'name': {'value': params['resource_name'], 'include': False},
+ })
+
+ policyuser = PolicyUser(nconfig, params['debug'])
+
+ # Run the oc adm policy user related command
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not policyuser.exists():
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a delete.'}
+
+ api_rval = policyuser.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results' : api_rval, state:'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ results = policyuser.exists()
+ if isinstance(results, dict) and 'returncode' in results and results['returncode'] != 0:
+ return {'msg': results}
+
+ if not results:
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: would have performed a create.'}
+
+ api_rval = policyuser.perform()
+
+ if api_rval['returncode'] != 0:
+ return {'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: 'present'}
+
+ return {'changed': False, state: 'present'}
+
+ return {'failed': True, 'changed': False, 'results': 'Unknown state passed. %s' % state, state: 'unknown'}
diff --git a/roles/lib_openshift/src/class/oc_adm_registry.py b/roles/lib_openshift/src/class/oc_adm_registry.py
new file mode 100644
index 000000000..ad6869bb6
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_registry.py
@@ -0,0 +1,430 @@
+# pylint: skip-file
+# flake8: noqa
+
+class RegistryException(Exception):
+ ''' Registry Exception Class '''
+ pass
+
+
+class RegistryConfig(OpenShiftCLIConfig):
+ ''' RegistryConfig is a DTO for the registry. '''
+ def __init__(self, rname, namespace, kubeconfig, registry_options):
+ super(RegistryConfig, self).__init__(rname, namespace, kubeconfig, registry_options)
+
+
+class Registry(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ volume_mount_path = 'spec.template.spec.containers[0].volumeMounts'
+ volume_path = 'spec.template.spec.volumes'
+ env_path = 'spec.template.spec.containers[0].env'
+
+ def __init__(self,
+ registry_config,
+ verbose=False):
+ ''' Constructor for Registry
+
+ a registry consists of 3 or more parts
+ - dc/docker-registry
+ - svc/docker-registry
+
+ Parameters:
+ :registry_config:
+ :verbose:
+ '''
+ super(Registry, self).__init__(registry_config.namespace, registry_config.kubeconfig, verbose)
+ self.version = OCVersion(registry_config.kubeconfig, verbose)
+ self.svc_ip = None
+ self.portal_ip = None
+ self.config = registry_config
+ self.verbose = verbose
+ self.registry_parts = [{'kind': 'dc', 'name': self.config.name},
+ {'kind': 'svc', 'name': self.config.name},
+ ]
+
+ self.__prepared_registry = None
+ self.volume_mounts = []
+ self.volumes = []
+ if self.config.config_options['volume_mounts']['value']:
+ for volume in self.config.config_options['volume_mounts']['value']:
+ volume_info = {'secret_name': volume.get('secret_name', None),
+ 'name': volume.get('name', None),
+ 'type': volume.get('type', None),
+ 'path': volume.get('path', None),
+ 'claimName': volume.get('claim_name', None),
+ 'claimSize': volume.get('claim_size', None),
+ }
+
+ vol, vol_mount = Volume.create_volume_structure(volume_info)
+ self.volumes.append(vol)
+ self.volume_mounts.append(vol_mount)
+
+ self.dconfig = None
+ self.svc = None
+
+ @property
+ def deploymentconfig(self):
+ ''' deploymentconfig property '''
+ return self.dconfig
+
+ @deploymentconfig.setter
+ def deploymentconfig(self, config):
+ ''' setter for deploymentconfig property '''
+ self.dconfig = config
+
+ @property
+ def service(self):
+ ''' service property '''
+ return self.svc
+
+ @service.setter
+ def service(self, config):
+ ''' setter for service property '''
+ self.svc = config
+
+ @property
+ def prepared_registry(self):
+ ''' prepared_registry property '''
+ if not self.__prepared_registry:
+ results = self.prepare_registry()
+ if not results or ('returncode' in results and results['returncode'] != 0):
+ raise RegistryException('Could not perform registry preparation. {}'.format(results))
+ self.__prepared_registry = results
+
+ return self.__prepared_registry
+
+ @prepared_registry.setter
+ def prepared_registry(self, data):
+ ''' setter method for prepared_registry attribute '''
+ self.__prepared_registry = data
+
+ def get(self):
+ ''' return the self.registry_parts '''
+ self.deploymentconfig = None
+ self.service = None
+
+ rval = 0
+ for part in self.registry_parts:
+ result = self._get(part['kind'], name=part['name'])
+ if result['returncode'] == 0 and part['kind'] == 'dc':
+ self.deploymentconfig = DeploymentConfig(result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'svc':
+ self.service = Service(result['results'][0])
+
+ if result['returncode'] != 0:
+ rval = result['returncode']
+
+
+ return {'returncode': rval, 'deploymentconfig': self.deploymentconfig, 'service': self.service}
+
+ def exists(self):
+ '''does the object exist?'''
+ if self.deploymentconfig and self.service:
+ return True
+
+ return False
+
+ def delete(self, complete=True):
+ '''return all pods '''
+ parts = []
+ for part in self.registry_parts:
+ if not complete and part['kind'] == 'svc':
+ continue
+ parts.append(self._delete(part['kind'], part['name']))
+
+ # Clean up returned results
+ rval = 0
+ for part in parts:
+ # pylint: disable=invalid-sequence-index
+ if 'returncode' in part and part['returncode'] != 0:
+ rval = part['returncode']
+
+ return {'returncode': rval, 'results': parts}
+
+ def prepare_registry(self):
+ ''' prepare a registry for instantiation '''
+ options = self.config.to_option_list(ascommalist='labels')
+
+ cmd = ['registry']
+ cmd.extend(options)
+ cmd.extend(['--dry-run=True', '-o', 'json'])
+
+ results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json')
+ # probably need to parse this
+ # pylint thinks results is a string
+ # pylint: disable=no-member
+ if results['returncode'] != 0 and 'items' not in results['results']:
+ raise RegistryException('Could not perform registry preparation. {}'.format(results))
+
+ service = None
+ deploymentconfig = None
+ # pylint: disable=invalid-sequence-index
+ for res in results['results']['items']:
+ if res['kind'] == 'DeploymentConfig':
+ deploymentconfig = DeploymentConfig(res)
+ elif res['kind'] == 'Service':
+ service = Service(res)
+
+ # Verify we got a service and a deploymentconfig
+ if not service or not deploymentconfig:
+ return results
+
+ # results will need to get parsed here and modifications added
+ deploymentconfig = DeploymentConfig(self.add_modifications(deploymentconfig))
+
+ # modify service ip
+ if self.svc_ip:
+ service.put('spec.clusterIP', self.svc_ip)
+ if self.portal_ip:
+ service.put('spec.portalIP', self.portal_ip)
+
+ # the dry-run doesn't apply the selector correctly
+ if self.service:
+ service.put('spec.selector', self.service.get_selector())
+
+ # need to create the service and the deploymentconfig
+ service_file = Utils.create_tmp_file_from_contents('service', service.yaml_dict)
+ deployment_file = Utils.create_tmp_file_from_contents('deploymentconfig', deploymentconfig.yaml_dict)
+
+ return {"service": service,
+ "service_file": service_file,
+ "service_update": False,
+ "deployment": deploymentconfig,
+ "deployment_file": deployment_file,
+ "deployment_update": False}
+
+ def create(self):
+ '''Create a registry'''
+ results = []
+ self.needs_update()
+ # if the object is none, then we need to create it
+ # if the object needs an update, then we should call replace
+ # Handle the deploymentconfig
+ if self.deploymentconfig is None:
+ results.append(self._create(self.prepared_registry['deployment_file']))
+ elif self.prepared_registry['deployment_update']:
+ results.append(self._replace(self.prepared_registry['deployment_file']))
+
+ # Handle the service
+ if self.service is None:
+ results.append(self._create(self.prepared_registry['service_file']))
+ elif self.prepared_registry['service_update']:
+ results.append(self._replace(self.prepared_registry['service_file']))
+
+ # Clean up returned results
+ rval = 0
+ for result in results:
+ # pylint: disable=invalid-sequence-index
+ if 'returncode' in result and result['returncode'] != 0:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ def update(self):
+ '''run update for the registry. This performs a replace if required'''
+ # Store the current service IP
+ if self.service:
+ svcip = self.service.get('spec.clusterIP')
+ if svcip:
+ self.svc_ip = svcip
+ portip = self.service.get('spec.portalIP')
+ if portip:
+ self.portal_ip = portip
+
+ results = []
+ if self.prepared_registry['deployment_update']:
+ results.append(self._replace(self.prepared_registry['deployment_file']))
+ if self.prepared_registry['service_update']:
+ results.append(self._replace(self.prepared_registry['service_file']))
+
+ # Clean up returned results
+ rval = 0
+ for result in results:
+ if result['returncode'] != 0:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ def add_modifications(self, deploymentconfig):
+ ''' update a deployment config with changes '''
+ # The environment variable for REGISTRY_HTTP_SECRET is autogenerated
+ # We should set the generated deploymentconfig to the in memory version
+ # the following modifications will overwrite if needed
+ if self.deploymentconfig:
+ result = self.deploymentconfig.get_env_var('REGISTRY_HTTP_SECRET')
+ if result:
+ deploymentconfig.update_env_var('REGISTRY_HTTP_SECRET', result['value'])
+
+ # Currently we know that our deployment of a registry requires a few extra modifications
+ # Modification 1
+ # we need specific environment variables to be set
+ for key, value in self.config.config_options['env_vars'].get('value', {}).items():
+ if not deploymentconfig.exists_env_key(key):
+ deploymentconfig.add_env_value(key, value)
+ else:
+ deploymentconfig.update_env_var(key, value)
+
+ # Modification 2
+ # we need specific volume variables to be set
+ for volume in self.volumes:
+ deploymentconfig.update_volume(volume)
+
+ for vol_mount in self.volume_mounts:
+ deploymentconfig.update_volume_mount(vol_mount)
+
+ # Modification 3
+ # Edits
+ edit_results = []
+ for edit in self.config.config_options['edits'].get('value', []):
+ if edit['action'] == 'put':
+ edit_results.append(deploymentconfig.put(edit['key'],
+ edit['value']))
+ if edit['action'] == 'update':
+ edit_results.append(deploymentconfig.update(edit['key'],
+ edit['value'],
+ edit.get('index', None),
+ edit.get('curr_value', None)))
+ if edit['action'] == 'append':
+ edit_results.append(deploymentconfig.append(edit['key'],
+ edit['value']))
+
+ if edit_results and not any([res[0] for res in edit_results]):
+ return None
+
+ return deploymentconfig.yaml_dict
+
+ def needs_update(self):
+ ''' check to see if we need to update '''
+ exclude_list = ['clusterIP', 'portalIP', 'type', 'protocol']
+ if self.service is None or \
+ not Utils.check_def_equal(self.prepared_registry['service'].yaml_dict,
+ self.service.yaml_dict,
+ exclude_list,
+ debug=self.verbose):
+ self.prepared_registry['service_update'] = True
+
+ exclude_list = ['dnsPolicy',
+ 'terminationGracePeriodSeconds',
+ 'restartPolicy', 'timeoutSeconds',
+ 'livenessProbe', 'readinessProbe',
+ 'terminationMessagePath',
+ 'securityContext',
+ 'imagePullPolicy',
+ 'protocol', # ports.portocol: TCP
+ 'type', # strategy: {'type': 'rolling'}
+ 'defaultMode', # added on secrets
+ 'activeDeadlineSeconds', # added in 1.5 for timeouts
+ ]
+
+ if self.deploymentconfig is None or \
+ not Utils.check_def_equal(self.prepared_registry['deployment'].yaml_dict,
+ self.deploymentconfig.yaml_dict,
+ exclude_list,
+ debug=self.verbose):
+ self.prepared_registry['deployment_update'] = True
+
+ return self.prepared_registry['deployment_update'] or self.prepared_registry['service_update'] or False
+
+ # In the future, we would like to break out each ansible state into a function.
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run idempotent ansible code'''
+
+ registry_options = {'images': {'value': params['images'], 'include': True},
+ 'latest_images': {'value': params['latest_images'], 'include': True},
+ 'labels': {'value': params['labels'], 'include': True},
+ 'ports': {'value': ','.join(params['ports']), 'include': True},
+ 'replicas': {'value': params['replicas'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'service_account': {'value': params['service_account'], 'include': True},
+ 'mount_host': {'value': params['mount_host'], 'include': True},
+ 'env_vars': {'value': params['env_vars'], 'include': False},
+ 'volume_mounts': {'value': params['volume_mounts'], 'include': False},
+ 'edits': {'value': params['edits'], 'include': False},
+ 'tls_key': {'value': params['tls_key'], 'include': True},
+ 'tls_certificate': {'value': params['tls_certificate'], 'include': True},
+ }
+
+ # Do not always pass the daemonset and enforce-quota parameters because they are not understood
+ # by old versions of oc.
+ # Default value is false. So, it's safe to not pass an explicit false value to oc versions which
+ # understand these parameters.
+ if params['daemonset']:
+ registry_options['daemonset'] = {'value': params['daemonset'], 'include': True}
+ if params['enforce_quota']:
+ registry_options['enforce_quota'] = {'value': params['enforce_quota'], 'include': True}
+
+ rconfig = RegistryConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ registry_options)
+
+
+ ocregistry = Registry(rconfig, params['debug'])
+
+ api_rval = ocregistry.get()
+
+ state = params['state']
+ ########
+ # get
+ ########
+ if state == 'list':
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not ocregistry.exists():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ # Unsure as to why this is angry with the return type.
+ # pylint: disable=redefined-variable-type
+ api_rval = ocregistry.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not ocregistry.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ api_rval = ocregistry.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if not params['force'] and not ocregistry.needs_update():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = ocregistry.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. %s' % state}
diff --git a/roles/lib_openshift/src/class/oc_adm_router.py b/roles/lib_openshift/src/class/oc_adm_router.py
new file mode 100644
index 000000000..0d50116d1
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_adm_router.py
@@ -0,0 +1,500 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class RouterException(Exception):
+ ''' Router exception'''
+ pass
+
+
+class RouterConfig(OpenShiftCLIConfig):
+ ''' RouterConfig is a DTO for the router. '''
+ def __init__(self, rname, namespace, kubeconfig, router_options):
+ super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options)
+
+
+class Router(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ def __init__(self,
+ router_config,
+ verbose=False):
+ ''' Constructor for OpenshiftOC
+
+ a router consists of 3 or more parts
+ - dc/router
+ - svc/router
+ - sa/router
+ - secret/router-certs
+ - clusterrolebinding/router-router-role
+ '''
+ super(Router, self).__init__('default', router_config.kubeconfig, verbose)
+ self.config = router_config
+ self.verbose = verbose
+ self.router_parts = [{'kind': 'dc', 'name': self.config.name},
+ {'kind': 'svc', 'name': self.config.name},
+ {'kind': 'sa', 'name': self.config.config_options['service_account']['value']},
+ {'kind': 'secret', 'name': self.config.name + '-certs'},
+ {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'},
+ ]
+
+ self.__prepared_router = None
+ self.dconfig = None
+ self.svc = None
+ self._secret = None
+ self._serviceaccount = None
+ self._rolebinding = None
+
+ @property
+ def prepared_router(self):
+ ''' property for the prepared router'''
+ if self.__prepared_router is None:
+ results = self._prepare_router()
+ if not results or 'returncode' in results and results['returncode'] != 0:
+ if 'stderr' in results:
+ raise RouterException('Could not perform router preparation: %s' % results['stderr'])
+
+ raise RouterException('Could not perform router preparation.')
+ self.__prepared_router = results
+
+ return self.__prepared_router
+
+ @prepared_router.setter
+ def prepared_router(self, obj):
+ '''setter for the prepared_router'''
+ self.__prepared_router = obj
+
+ @property
+ def deploymentconfig(self):
+ ''' property deploymentconfig'''
+ return self.dconfig
+
+ @deploymentconfig.setter
+ def deploymentconfig(self, config):
+ ''' setter for property deploymentconfig '''
+ self.dconfig = config
+
+ @property
+ def service(self):
+ ''' property for service '''
+ return self.svc
+
+ @service.setter
+ def service(self, config):
+ ''' setter for property service '''
+ self.svc = config
+
+ @property
+ def secret(self):
+ ''' property secret '''
+ return self._secret
+
+ @secret.setter
+ def secret(self, config):
+ ''' setter for property secret '''
+ self._secret = config
+
+ @property
+ def serviceaccount(self):
+ ''' property for serviceaccount '''
+ return self._serviceaccount
+
+ @serviceaccount.setter
+ def serviceaccount(self, config):
+ ''' setter for property serviceaccount '''
+ self._serviceaccount = config
+
+ @property
+ def rolebinding(self):
+ ''' property rolebinding '''
+ return self._rolebinding
+
+ @rolebinding.setter
+ def rolebinding(self, config):
+ ''' setter for property rolebinding '''
+ self._rolebinding = config
+
+ def get_object_by_kind(self, kind):
+ '''return the current object kind by name'''
+ if re.match("^(dc|deploymentconfig)$", kind, flags=re.IGNORECASE):
+ return self.deploymentconfig
+ elif re.match("^(svc|service)$", kind, flags=re.IGNORECASE):
+ return self.service
+ elif re.match("^(sa|serviceaccount)$", kind, flags=re.IGNORECASE):
+ return self.serviceaccount
+ elif re.match("secret", kind, flags=re.IGNORECASE):
+ return self.secret
+ elif re.match("clusterrolebinding", kind, flags=re.IGNORECASE):
+ return self.rolebinding
+
+ return None
+
+ def get(self):
+ ''' return the self.router_parts '''
+ self.service = None
+ self.deploymentconfig = None
+ self.serviceaccount = None
+ self.secret = None
+ self.rolebinding = None
+ for part in self.router_parts:
+ result = self._get(part['kind'], name=part['name'])
+ if result['returncode'] == 0 and part['kind'] == 'dc':
+ self.deploymentconfig = DeploymentConfig(result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'svc':
+ self.service = Service(content=result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'sa':
+ self.serviceaccount = ServiceAccount(content=result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'secret':
+ self.secret = Secret(content=result['results'][0])
+ elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding':
+ self.rolebinding = RoleBinding(content=result['results'][0])
+
+ return {'deploymentconfig': self.deploymentconfig,
+ 'service': self.service,
+ 'serviceaccount': self.serviceaccount,
+ 'secret': self.secret,
+ 'clusterrolebinding': self.rolebinding,
+ }
+
+ def exists(self):
+ '''return a whether svc or dc exists '''
+ if self.deploymentconfig and self.service and self.secret and self.serviceaccount:
+ return True
+
+ return False
+
+ def delete(self):
+ '''return all pods '''
+ parts = []
+ for part in self.router_parts:
+ parts.append(self._delete(part['kind'], part['name']))
+
+ rval = 0
+ for part in parts:
+ if part['returncode'] != 0 and not 'already exist' in part['stderr']:
+ rval = part['returncode']
+
+ return {'returncode': rval, 'results': parts}
+
+ def add_modifications(self, deploymentconfig):
+ '''modify the deployment config'''
+ # We want modifications in the form of edits coming in from the module.
+ # Let's apply these here
+ edit_results = []
+ for edit in self.config.config_options['edits'].get('value', []):
+ if edit['action'] == 'put':
+ edit_results.append(deploymentconfig.put(edit['key'],
+ edit['value']))
+ if edit['action'] == 'update':
+ edit_results.append(deploymentconfig.update(edit['key'],
+ edit['value'],
+ edit.get('index', None),
+ edit.get('curr_value', None)))
+ if edit['action'] == 'append':
+ edit_results.append(deploymentconfig.append(edit['key'],
+ edit['value']))
+
+ if edit_results and not any([res[0] for res in edit_results]):
+ return None
+
+ return deploymentconfig
+
+ # pylint: disable=too-many-branches
+ def _prepare_router(self):
+ '''prepare router for instantiation'''
+ # if cacert, key, and cert were passed, combine them into a pem file
+ if (self.config.config_options['cacert_file']['value'] and
+ self.config.config_options['cert_file']['value'] and
+ self.config.config_options['key_file']['value']):
+
+ router_pem = '/tmp/router.pem'
+ with open(router_pem, 'w') as rfd:
+ rfd.write(open(self.config.config_options['cert_file']['value']).read())
+ rfd.write(open(self.config.config_options['key_file']['value']).read())
+ if self.config.config_options['cacert_file']['value'] and \
+ os.path.exists(self.config.config_options['cacert_file']['value']):
+ rfd.write(open(self.config.config_options['cacert_file']['value']).read())
+
+ atexit.register(Utils.cleanup, [router_pem])
+
+ self.config.config_options['default_cert']['value'] = router_pem
+
+ elif self.config.config_options['default_cert']['value'] is None:
+ # No certificate was passed to us. do not pass one to oc adm router
+ self.config.config_options['default_cert']['include'] = False
+
+ options = self.config.to_option_list(ascommalist='labels')
+
+ cmd = ['router', self.config.name]
+ cmd.extend(options)
+ cmd.extend(['--dry-run=True', '-o', 'json'])
+
+ results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json')
+
+ # pylint: disable=maybe-no-member
+ if results['returncode'] != 0 or 'items' not in results['results']:
+ return results
+
+ oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False},
+ 'Secret': {'obj': None, 'path': None, 'update': False},
+ 'ServiceAccount': {'obj': None, 'path': None, 'update': False},
+ 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False},
+ 'Service': {'obj': None, 'path': None, 'update': False},
+ }
+ # pylint: disable=invalid-sequence-index
+ for res in results['results']['items']:
+ if res['kind'] == 'DeploymentConfig':
+ oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res)
+ elif res['kind'] == 'Service':
+ oc_objects['Service']['obj'] = Service(res)
+ elif res['kind'] == 'ServiceAccount':
+ oc_objects['ServiceAccount']['obj'] = ServiceAccount(res)
+ elif res['kind'] == 'Secret':
+ oc_objects['Secret']['obj'] = Secret(res)
+ elif res['kind'] == 'ClusterRoleBinding':
+ oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res)
+
+ # Currently only deploymentconfig needs updating
+ # Verify we got a deploymentconfig
+ if not oc_objects['DeploymentConfig']['obj']:
+ return results
+
+ # add modifications added
+ oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj'])
+
+ for oc_type, oc_data in oc_objects.items():
+ if oc_data['obj'] is not None:
+ oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict)
+
+ return oc_objects
+
+ def create(self):
+ '''Create a router
+
+ This includes the different parts:
+ - deploymentconfig
+ - service
+ - serviceaccount
+ - secrets
+ - clusterrolebinding
+ '''
+ results = []
+ self.needs_update()
+
+ import time
+ # pylint: disable=maybe-no-member
+ for kind, oc_data in self.prepared_router.items():
+ if oc_data['obj'] is not None:
+ time.sleep(1)
+ if self.get_object_by_kind(kind) is None:
+ results.append(self._create(oc_data['path']))
+
+ elif oc_data['update']:
+ results.append(self._replace(oc_data['path']))
+
+
+ rval = 0
+ for result in results:
+ if result['returncode'] != 0 and not 'already exist' in result['stderr']:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ def update(self):
+ '''run update for the router. This performs a replace'''
+ results = []
+
+ # pylint: disable=maybe-no-member
+ for _, oc_data in self.prepared_router.items():
+ if oc_data['update']:
+ results.append(self._replace(oc_data['path']))
+
+ rval = 0
+ for result in results:
+ if result['returncode'] != 0:
+ rval = result['returncode']
+
+ return {'returncode': rval, 'results': results}
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ def needs_update(self):
+ ''' check to see if we need to update '''
+ # ServiceAccount:
+ # Need to determine changes from the pregenerated ones from the original
+ # Since these are auto generated, we can skip
+ skip = ['secrets', 'imagePullSecrets']
+ if self.serviceaccount is None or \
+ not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict,
+ self.serviceaccount.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['ServiceAccount']['update'] = True
+
+ # Secret:
+ # See if one was generated from our dry-run and verify it if needed
+ if self.prepared_router['Secret']['obj']:
+ if not self.secret:
+ self.prepared_router['Secret']['update'] = True
+
+ if self.secret is None or \
+ not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict,
+ self.secret.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['Secret']['update'] = True
+
+ # Service:
+ # Fix the ports to have protocol=TCP
+ for port in self.prepared_router['Service']['obj'].get('spec.ports'):
+ port['protocol'] = 'TCP'
+
+ skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type']
+ if self.service is None or \
+ not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict,
+ self.service.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['Service']['update'] = True
+
+ # DeploymentConfig:
+ # Router needs some exceptions.
+ # We do not want to check the autogenerated password for stats admin
+ if self.deploymentconfig is not None:
+ if not self.config.config_options['stats_password']['value']:
+ for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
+ 'spec.template.spec.containers[0].env') or []):
+ if env_var['name'] == 'STATS_PASSWORD':
+ env_var['value'] = \
+ self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx)
+ break
+
+ # dry-run doesn't add the protocol to the ports section. We will manually do that.
+ for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
+ 'spec.template.spec.containers[0].ports') or []):
+ if not 'protocol' in port:
+ port['protocol'] = 'TCP'
+
+ # These are different when generating
+ skip = ['dnsPolicy',
+ 'terminationGracePeriodSeconds',
+ 'restartPolicy', 'timeoutSeconds',
+ 'livenessProbe', 'readinessProbe',
+ 'terminationMessagePath', 'hostPort',
+ 'defaultMode',
+ ]
+
+ if self.deploymentconfig is None or \
+ not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict,
+ self.deploymentconfig.yaml_dict,
+ skip_keys=skip,
+ debug=self.verbose):
+ self.prepared_router['DeploymentConfig']['update'] = True
+
+ # Check if any of the parts need updating, if so, return True
+ # else, no need to update
+ # pylint: disable=no-member
+ return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()])
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run ansible idempotent code'''
+
+ rconfig = RouterConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ {'default_cert': {'value': params['default_cert'], 'include': True},
+ 'cert_file': {'value': params['cert_file'], 'include': False},
+ 'key_file': {'value': params['key_file'], 'include': False},
+ 'images': {'value': params['images'], 'include': True},
+ 'latest_images': {'value': params['latest_images'], 'include': True},
+ 'labels': {'value': params['labels'], 'include': True},
+ 'ports': {'value': ','.join(params['ports']), 'include': True},
+ 'replicas': {'value': params['replicas'], 'include': True},
+ 'selector': {'value': params['selector'], 'include': True},
+ 'service_account': {'value': params['service_account'], 'include': True},
+ 'router_type': {'value': params['router_type'], 'include': False},
+ 'host_network': {'value': params['host_network'], 'include': True},
+ 'external_host': {'value': params['external_host'], 'include': True},
+ 'external_host_vserver': {'value': params['external_host_vserver'],
+ 'include': True},
+ 'external_host_insecure': {'value': params['external_host_insecure'],
+ 'include': True},
+ 'external_host_partition_path': {'value': params['external_host_partition_path'],
+ 'include': True},
+ 'external_host_username': {'value': params['external_host_username'],
+ 'include': True},
+ 'external_host_password': {'value': params['external_host_password'],
+ 'include': True},
+ 'external_host_private_key': {'value': params['external_host_private_key'],
+ 'include': True},
+ 'expose_metrics': {'value': params['expose_metrics'], 'include': True},
+ 'metrics_image': {'value': params['metrics_image'], 'include': True},
+ 'stats_user': {'value': params['stats_user'], 'include': True},
+ 'stats_password': {'value': params['stats_password'], 'include': True},
+ 'stats_port': {'value': params['stats_port'], 'include': True},
+ # extra
+ 'cacert_file': {'value': params['cacert_file'], 'include': False},
+ # edits
+ 'edits': {'value': params['edits'], 'include': False},
+ })
+
+
+ state = params['state']
+
+ ocrouter = Router(rconfig, verbose=params['debug'])
+
+ api_rval = ocrouter.get()
+
+ ########
+ # get
+ ########
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not ocrouter.exists():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ # In case of delete we return a list of each object
+ # that represents a router and its result in a list
+ # pylint: disable=redefined-variable-type
+ api_rval = ocrouter.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not ocrouter.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ api_rval = ocrouter.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if not ocrouter.needs_update():
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = ocrouter.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
diff --git a/roles/lib_openshift/src/class/oc_clusterrole.py b/roles/lib_openshift/src/class/oc_clusterrole.py
new file mode 100644
index 000000000..328e5cb67
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_clusterrole.py
@@ -0,0 +1,167 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCClusterRole(OpenShiftCLI):
+ ''' Class to manage clusterrole objects'''
+ kind = 'clusterrole'
+
+ def __init__(self,
+ name,
+ rules=None,
+ kubeconfig=None,
+ verbose=False):
+ ''' Constructor for OCClusterRole '''
+ super(OCClusterRole, self).__init__(None, kubeconfig=kubeconfig, verbose=verbose)
+ self.verbose = verbose
+ self.name = name
+ self._clusterrole = None
+ self._inc_clusterrole = ClusterRole.builder(name, rules)
+
+ @property
+ def clusterrole(self):
+ ''' property for clusterrole'''
+ if self._clusterrole is None:
+ self.get()
+ return self._clusterrole
+
+ @clusterrole.setter
+ def clusterrole(self, data):
+ ''' setter function for clusterrole property'''
+ self._clusterrole = data
+
+ @property
+ def inc_clusterrole(self):
+ ''' property for inc_clusterrole'''
+ return self._inc_clusterrole
+
+ @inc_clusterrole.setter
+ def inc_clusterrole(self, data):
+ ''' setter function for inc_clusterrole property'''
+ self._inc_clusterrole = data
+
+ def exists(self):
+ ''' return whether a clusterrole exists '''
+ if self.clusterrole:
+ return True
+
+ return False
+
+ def get(self):
+ '''return a clusterrole '''
+ result = self._get(self.kind, self.name)
+
+ if result['returncode'] == 0:
+ self.clusterrole = ClusterRole(content=result['results'][0])
+ result['results'] = self.clusterrole.yaml_dict
+
+ elif '"{}" not found'.format(self.name) in result['stderr']:
+ result['returncode'] = 0
+ self.clusterrole = None
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.name)
+
+ def create(self):
+ '''create a clusterrole from the proposed incoming clusterrole'''
+ return self._create_from_content(self.name, self.inc_clusterrole.yaml_dict)
+
+ def update(self):
+ '''update a project'''
+ return self._replace_content(self.kind, self.name, self.inc_clusterrole.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed'''
+ return not self.clusterrole.compare(self.inc_clusterrole, self.verbose)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ oc_clusterrole = OCClusterRole(params['name'],
+ params['rules'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_clusterrole.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_clusterrole.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_clusterrole.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_clusterrole.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_clusterrole.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_clusterrole.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_clusterrole.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = oc_clusterrole.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_clusterrole.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. [%s]' % state}
diff --git a/roles/lib_openshift/src/class/oc_configmap.py b/roles/lib_openshift/src/class/oc_configmap.py
new file mode 100644
index 000000000..de77d1102
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_configmap.py
@@ -0,0 +1,191 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-arguments
+class OCConfigMap(OpenShiftCLI):
+ ''' Openshift ConfigMap Class
+
+ ConfigMaps are a way to store data inside of objects
+ '''
+ def __init__(self,
+ name,
+ from_file,
+ from_literal,
+ state,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCConfigMap, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = name
+ self.state = state
+ self._configmap = None
+ self._inc_configmap = None
+ self.from_file = from_file if from_file is not None else {}
+ self.from_literal = from_literal if from_literal is not None else {}
+
+ @property
+ def configmap(self):
+ if self._configmap is None:
+ self._configmap = self.get()
+
+ return self._configmap
+
+ @configmap.setter
+ def configmap(self, inc_map):
+ self._configmap = inc_map
+
+ @property
+ def inc_configmap(self):
+ if self._inc_configmap is None:
+ results = self.create(dryrun=True, output=True)
+ self._inc_configmap = results['results']
+
+ return self._inc_configmap
+
+ @inc_configmap.setter
+ def inc_configmap(self, inc_map):
+ self._inc_configmap = inc_map
+
+ def from_file_to_params(self):
+ '''return from_files in a string ready for cli'''
+ return ["--from-file={}={}".format(key, value) for key, value in self.from_file.items()]
+
+ def from_literal_to_params(self):
+ '''return from_literal in a string ready for cli'''
+ return ["--from-literal={}={}".format(key, value) for key, value in self.from_literal.items()]
+
+ def get(self):
+ '''return a configmap by name '''
+ results = self._get('configmap', self.name)
+ if results['returncode'] == 0 and results['results'][0]:
+ self.configmap = results['results'][0]
+
+ if results['returncode'] != 0 and '"{}" not found'.format(self.name) in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete a configmap by name'''
+ return self._delete('configmap', self.name)
+
+ def create(self, dryrun=False, output=False):
+ '''Create a configmap
+
+ :dryrun: Product what you would have done. default: False
+ :output: Whether to parse output. default: False
+ '''
+
+ cmd = ['create', 'configmap', self.name]
+ if self.from_literal is not None:
+ cmd.extend(self.from_literal_to_params())
+
+ if self.from_file is not None:
+ cmd.extend(self.from_file_to_params())
+
+ if dryrun:
+ cmd.extend(['--dry-run', '-ojson'])
+
+ results = self.openshift_cmd(cmd, output=output)
+
+ return results
+
+ def update(self):
+ '''run update configmap '''
+ return self._replace_content('configmap', self.name, self.inc_configmap)
+
+ def needs_update(self):
+ '''compare the current configmap with the proposed and return if they are equal'''
+ return not Utils.check_def_equal(self.inc_configmap, self.configmap, debug=self.verbose)
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ oc_cm = OCConfigMap(params['name'],
+ params['from_file'],
+ params['from_literal'],
+ params['state'],
+ params['namespace'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_cm.get()
+
+ if 'failed' in api_rval:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if not params['name']:
+ return {'failed': True,
+ 'msg': 'Please specify a name when state is absent|present.'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_cm.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Create
+ ########
+ if state == 'present':
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ api_rval = oc_cm.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ api_rval = oc_cm.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_cm.needs_update():
+
+ api_rval = oc_cm.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ api_rval = oc_cm.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
diff --git a/roles/lib_openshift/src/class/oc_edit.py b/roles/lib_openshift/src/class/oc_edit.py
new file mode 100644
index 000000000..629e5a007
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_edit.py
@@ -0,0 +1,91 @@
+# pylint: skip-file
+# flake8: noqa
+
+class Edit(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools
+ '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ namespace,
+ resource_name=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ separator='.',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(Edit, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.kind = kind
+ self.name = resource_name
+ self.separator = separator
+
+ def get(self):
+ '''return a secret by name '''
+ return self._get(self.kind, self.name)
+
+ def update(self, file_name, content, force=False, content_type='yaml'):
+ '''run update '''
+ if file_name:
+ if content_type == 'yaml':
+ data = yaml.load(open(file_name))
+ elif content_type == 'json':
+ data = json.loads(open(file_name).read())
+
+ changes = []
+ yed = Yedit(filename=file_name, content=data, separator=self.separator)
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([not change[0] for change in changes]):
+ return {'returncode': 0, 'updated': False}
+
+ yed.write()
+
+ atexit.register(Utils.cleanup, [file_name])
+
+ return self._replace(file_name, force=force)
+
+ return self._replace_content(self.kind, self.name, content, force=force, sep=self.separator)
+
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ ocedit = Edit(params['kind'],
+ params['namespace'],
+ params['name'],
+ kubeconfig=params['kubeconfig'],
+ separator=params['separator'],
+ verbose=params['debug'])
+
+ api_rval = ocedit.get()
+
+ ########
+ # Create
+ ########
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {"failed": True, 'msg': api_rval}
+
+ ########
+ # Update
+ ########
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed edit'}
+
+ api_rval = ocedit.update(params['file_name'],
+ params['content'],
+ params['force'],
+ params['file_format'])
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, 'msg': api_rval}
+
+ if 'updated' in api_rval and not api_rval['updated']:
+ return {"changed": False, 'results': api_rval, 'state': 'present'}
+
+ # return the created object
+ api_rval = ocedit.get()
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, 'msg': api_rval}
+
+ return {"changed": True, 'results': api_rval, 'state': 'present'}
diff --git a/roles/lib_openshift/src/class/oc_env.py b/roles/lib_openshift/src/class/oc_env.py
new file mode 100644
index 000000000..748b46cb5
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_env.py
@@ -0,0 +1,142 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCEnv(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ container_path = {"pod": "spec.containers[0].env",
+ "dc": "spec.template.spec.containers[0].env",
+ "rc": "spec.template.spec.containers[0].env",
+ }
+
+ # pylint allows 5. we need 6
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ namespace,
+ kind,
+ env_vars,
+ resource_name=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCEnv, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.kind = kind
+ self.name = resource_name
+ self.env_vars = env_vars
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var'''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var'''
+ self._resource = data
+
+ def key_value_exists(self, key, value):
+ ''' return whether a key, value pair exists '''
+ return self.resource.exists_env_value(key, value)
+
+ def key_exists(self, key):
+ ''' return whether a key exists '''
+ return self.resource.exists_env_key(key)
+
+ def get(self):
+ '''return environment variables '''
+ result = self._get(self.kind, self.name)
+ if result['returncode'] == 0:
+ if self.kind == 'dc':
+ self.resource = DeploymentConfig(content=result['results'][0])
+ result['results'] = self.resource.get(OCEnv.container_path[self.kind]) or []
+ return result
+
+ def delete(self):
+ ''' delete environment variables '''
+ if self.resource.delete_env_var(self.env_vars.keys()):
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ return {'returncode': 0, 'changed': False}
+
+ def put(self):
+ '''place env vars into dc '''
+ for update_key, update_value in self.env_vars.items():
+ self.resource.update_env_var(update_key, update_value)
+
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ ocenv = OCEnv(params['namespace'],
+ params['kind'],
+ params['env_vars'],
+ resource_name=params['name'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocenv.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ for key in params.get('env_vars', {}).keys():
+ if ocenv.resource.exists_env_key(key):
+
+ if check_mode:
+ return {'changed': False,
+ 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = ocenv.delete()
+
+ return {'changed': True, 'state': 'absent'}
+
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ for key, value in params.get('env_vars', {}).items():
+ if not ocenv.key_value_exists(key, value):
+
+ if check_mode:
+ return {'changed': False,
+ 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = ocenv.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = ocenv.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval['results'], 'state': 'present'}
+
+ return {'changed': False, 'results': api_rval['results'], 'state': 'present'}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state}
diff --git a/roles/lib_openshift/src/class/oc_group.py b/roles/lib_openshift/src/class/oc_group.py
new file mode 100644
index 000000000..89fb09ea4
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_group.py
@@ -0,0 +1,148 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class OCGroup(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'group'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCGroup '''
+ super(OCGroup, self).__init__(config.namespace, config.kubeconfig)
+ self.config = config
+ self.namespace = config.namespace
+ self._group = None
+
+ @property
+ def group(self):
+ ''' property function service'''
+ if not self._group:
+ self.get()
+ return self._group
+
+ @group.setter
+ def group(self, data):
+ ''' setter function for yedit var '''
+ self._group = data
+
+ def exists(self):
+ ''' return whether a group exists '''
+ if self.group:
+ return True
+
+ return False
+
+ def get(self):
+ '''return group information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.group = Group(content=result['results'][0])
+ elif 'groups \"{}\" not found'.format(self.config.name) in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ return not Utils.check_def_equal(self.config.data, self.group.yaml_dict, skip_keys=[], debug=True)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+
+ gconfig = GroupConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ )
+ oc_group = OCGroup(gconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_group.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_group.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_group.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_group.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_group.needs_update():
+ api_rval = oc_group.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_group.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
diff --git a/roles/lib_openshift/src/class/oc_image.py b/roles/lib_openshift/src/class/oc_image.py
new file mode 100644
index 000000000..d25349127
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_image.py
@@ -0,0 +1,91 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-arguments
+class OCImage(OpenShiftCLI):
+ ''' Class to import and create an imagestream object'''
+ def __init__(self,
+ namespace,
+ registry_url,
+ image_name,
+ image_tag,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCImage'''
+ super(OCImage, self).__init__(namespace, kubeconfig)
+ self.registry_url = registry_url
+ self.image_name = image_name
+ self.image_tag = image_tag
+ self.verbose = verbose
+
+ def get(self):
+ '''return a image by name '''
+ results = self._get('imagestream', self.image_name)
+ results['exists'] = False
+ if results['returncode'] == 0 and results['results'][0]:
+ results['exists'] = True
+
+ if results['returncode'] != 0 and '"{}" not found'.format(self.image_name) in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def create(self, url=None, name=None, tag=None):
+ '''Create an image '''
+ return self._import_image(url, name, tag)
+
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ ''' run the ansible idempotent code '''
+
+ ocimage = OCImage(params['namespace'],
+ params['registry_url'],
+ params['image_name'],
+ params['image_tag'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocimage.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+ return {"changed": False, "results": api_rval, "state": "list"}
+
+ ########
+ # Create
+ ########
+ if state == 'present':
+
+ if not Utils.exists(api_rval['results'], params['image_name']):
+
+ if check_mode:
+ return {"changed": False, "msg": 'CHECK_MODE: Would have performed a create'}
+
+ api_rval = ocimage.create(params['registry_url'],
+ params['image_name'],
+ params['image_tag'])
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ # return the newly created object
+ api_rval = ocimage.get()
+
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ return {"changed": True, "results": api_rval, "state": "present"}
+
+ # image exists, no change
+ return {"changed": False, "results": api_rval, "state": "present"}
+
+ return {"failed": True, "changed": False, "msg": "Unknown state passed. {0}".format(state)}
diff --git a/roles/lib_openshift/src/class/oc_label.py b/roles/lib_openshift/src/class/oc_label.py
new file mode 100644
index 000000000..0a6895177
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_label.py
@@ -0,0 +1,292 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCLabel(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kind,
+ kubeconfig,
+ labels=None,
+ selector=None,
+ verbose=False):
+ ''' Constructor for OCLabel '''
+ super(OCLabel, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = name
+ self.kind = kind
+ self.labels = labels
+ self._curr_labels = None
+ self.selector = selector
+
+ @property
+ def current_labels(self):
+ '''property for the current labels'''
+ if self._curr_labels is None:
+ results = self.get()
+ self._curr_labels = results['labels']
+
+ return self._curr_labels
+
+ @current_labels.setter
+ def current_labels(self, data):
+ '''property setter for current labels'''
+ self._curr_labels = data
+
+ def compare_labels(self, host_labels):
+ ''' compare incoming labels against current labels'''
+
+ for label in self.labels:
+ if label['key'] not in host_labels or \
+ label['value'] != host_labels[label['key']]:
+ return False
+ return True
+
+ def all_user_labels_exist(self):
+ ''' return whether all the labels already exist '''
+
+ for current_host_labels in self.current_labels:
+ rbool = self.compare_labels(current_host_labels)
+ if not rbool:
+ return False
+ return True
+
+ def any_label_exists(self):
+ ''' return whether any single label already exists '''
+
+ for current_host_labels in self.current_labels:
+ for label in self.labels:
+ if label['key'] in current_host_labels:
+ return True
+ return False
+
+ def get_user_keys(self):
+ ''' go through list of user key:values and return all keys '''
+
+ user_keys = []
+ for label in self.labels:
+ user_keys.append(label['key'])
+
+ return user_keys
+
+ def get_current_label_keys(self):
+ ''' collect all the current label keys '''
+
+ current_label_keys = []
+ for current_host_labels in self.current_labels:
+ for key in current_host_labels.keys():
+ current_label_keys.append(key)
+
+ return list(set(current_label_keys))
+
+ def get_extra_current_labels(self):
+ ''' return list of labels that are currently stored, but aren't
+ in user-provided list '''
+
+ extra_labels = []
+ user_label_keys = self.get_user_keys()
+ current_label_keys = self.get_current_label_keys()
+
+ for current_key in current_label_keys:
+ if current_key not in user_label_keys:
+ extra_labels.append(current_key)
+
+ return extra_labels
+
+ def extra_current_labels(self):
+ ''' return whether there are labels currently stored that user
+ hasn't directly provided '''
+ extra_labels = self.get_extra_current_labels()
+
+ if len(extra_labels) > 0:
+ return True
+
+ return False
+
+ def replace(self):
+ ''' replace currently stored labels with user provided labels '''
+ cmd = self.cmd_template()
+
+ # First delete any extra labels
+ extra_labels = self.get_extra_current_labels()
+ if len(extra_labels) > 0:
+ for label in extra_labels:
+ cmd.append("{}-".format(label))
+
+ # Now add/modify the user-provided label list
+ if len(self.labels) > 0:
+ for label in self.labels:
+ cmd.append("{}={}".format(label['key'], label['value']))
+
+ # --overwrite for the case where we are updating existing labels
+ cmd.append("--overwrite")
+ return self.openshift_cmd(cmd)
+
+ def get(self):
+ '''return label information '''
+
+ result_dict = {}
+ label_list = []
+
+ if self.name:
+ result = self._get(resource=self.kind, name=self.name, selector=self.selector)
+
+ if result['results'][0] and 'labels' in result['results'][0]['metadata']:
+ label_list.append(result['results'][0]['metadata']['labels'])
+ else:
+ label_list.append({})
+
+ else:
+ result = self._get(resource=self.kind, selector=self.selector)
+
+ for item in result['results'][0]['items']:
+ if 'labels' in item['metadata']:
+ label_list.append(item['metadata']['labels'])
+ else:
+ label_list.append({})
+
+ self.current_labels = label_list
+ result_dict['labels'] = self.current_labels
+ result_dict['item_count'] = len(self.current_labels)
+ result['results'] = result_dict
+
+ return result
+
+ def cmd_template(self):
+ ''' boilerplate oc command for modifying lables on this object '''
+ # let's build the cmd with what we have passed in
+ cmd = ["label", self.kind]
+
+ if self.selector:
+ cmd.extend(["--selector", self.selector])
+ elif self.name:
+ cmd.extend([self.name])
+
+ return cmd
+
+ def add(self):
+ ''' add labels '''
+ cmd = self.cmd_template()
+
+ for label in self.labels:
+ cmd.append("{}={}".format(label['key'], label['value']))
+
+ cmd.append("--overwrite")
+
+ return self.openshift_cmd(cmd)
+
+ def delete(self):
+ '''delete the labels'''
+ cmd = self.cmd_template()
+ for label in self.labels:
+ cmd.append("{}-".format(label['key']))
+
+ return self.openshift_cmd(cmd)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent ansible code
+
+ prams comes from the ansible portion of this module
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+ oc_label = OCLabel(params['name'],
+ params['namespace'],
+ params['kind'],
+ params['kubeconfig'],
+ params['labels'],
+ params['selector'],
+ verbose=params['debug'])
+
+ state = params['state']
+ name = params['name']
+ selector = params['selector']
+
+ api_rval = oc_label.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ #######
+ # Add
+ #######
+ if state == 'add':
+ if not (name or selector):
+ return {'failed': True,
+ 'msg': "Param 'name' or 'selector' is required if state == 'add'"}
+ if not oc_label.all_user_labels_exist():
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed an addition.'}
+ api_rval = oc_label.add()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "add"}
+
+ return {'changed': False, 'state': "add"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not (name or selector):
+ return {'failed': True,
+ 'msg': "Param 'name' or 'selector' is required if state == 'absent'"}
+
+ if oc_label.any_label_exists():
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_label.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Update
+ ########
+ if not (name or selector):
+ return {'failed': True,
+ 'msg': "Param 'name' or 'selector' is required if state == 'present'"}
+ # if all the labels passed in don't already exist
+ # or if there are currently stored labels that haven't
+ # been passed in
+ if not oc_label.all_user_labels_exist() or \
+ oc_label.extra_current_labels():
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have made changes.'}
+
+ api_rval = oc_label.replace()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_label.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+ return {'failed': True,
+ 'changed': False,
+ 'results': 'Unknown state passed. %s' % state,
+ 'state': "unknown"}
diff --git a/roles/lib_openshift/src/class/oc_obj.py b/roles/lib_openshift/src/class/oc_obj.py
new file mode 100644
index 000000000..68f7818e4
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_obj.py
@@ -0,0 +1,210 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class OCObject(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5. we need 6
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ namespace,
+ name=None,
+ selector=None,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCObject, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose,
+ all_namespaces=all_namespaces)
+ self.kind = kind
+ self.name = name
+ self.selector = selector
+
+ def get(self):
+ '''return a kind by name '''
+ results = self._get(self.kind, name=self.name, selector=self.selector)
+ if (results['returncode'] != 0 and 'stderr' in results and
+ '\"{}\" not found'.format(self.name) in results['stderr']):
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete the object'''
+ results = self._delete(self.kind, name=self.name, selector=self.selector)
+ if (results['returncode'] != 0 and 'stderr' in results and
+ '\"{}\" not found'.format(self.name) in results['stderr']):
+ results['returncode'] = 0
+
+ return results
+
+ def create(self, files=None, content=None):
+ '''
+ Create a config
+
+ NOTE: This creates the first file OR the first conent.
+ TODO: Handle all files and content passed in
+ '''
+ if files:
+ return self._create(files[0])
+
+ # pylint: disable=no-member
+ # The purpose of this change is twofold:
+ # - we need a check to only use the ruamel specific dumper if ruamel is loaded
+ # - the dumper or the flow style change is needed so openshift is able to parse
+ # the resulting yaml, at least until gopkg.in/yaml.v2 is updated
+ if hasattr(yaml, 'RoundTripDumper'):
+ content['data'] = yaml.dump(content['data'], Dumper=yaml.RoundTripDumper)
+ else:
+ content['data'] = yaml.safe_dump(content['data'], default_flow_style=False)
+
+ content_file = Utils.create_tmp_files_from_contents(content)[0]
+
+ return self._create(content_file['path'])
+
+ # pylint: disable=too-many-function-args
+ def update(self, files=None, content=None, force=False):
+ '''update a current openshift object
+
+ This receives a list of file names or content
+ and takes the first and calls replace.
+
+ TODO: take an entire list
+ '''
+ if files:
+ return self._replace(files[0], force)
+
+ if content and 'data' in content:
+ content = content['data']
+
+ return self.update_content(content, force)
+
+ def update_content(self, content, force=False):
+ '''update an object through using the content param'''
+ return self._replace_content(self.kind, self.name, content, force=force)
+
+ def needs_update(self, files=None, content=None, content_type='yaml'):
+ ''' check to see if we need to update '''
+ objects = self.get()
+ if objects['returncode'] != 0:
+ return objects
+
+ data = None
+ if files:
+ data = Utils.get_resource_file(files[0], content_type)
+ elif content and 'data' in content:
+ data = content['data']
+ else:
+ data = content
+
+ # if equal then no need. So not equal is True
+ return not Utils.check_def_equal(data, objects['results'][0], skip_keys=None, debug=False)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''perform the ansible idempotent code'''
+
+ ocobj = OCObject(params['kind'],
+ params['namespace'],
+ params['name'],
+ params['selector'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'],
+ all_namespaces=params['all_namespaces'])
+
+ state = params['state']
+
+ api_rval = ocobj.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ # verify its not in our results
+ if (params['name'] is not None or params['selector'] is not None) and \
+ (len(api_rval['results']) == 0 or \
+ ('items' in api_rval['results'][0] and len(api_rval['results'][0]['items']) == 0)):
+ return {'changed': False, 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete'}
+
+ api_rval = ocobj.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ # create/update: Must define a name beyond this point
+ if not params['name']:
+ return {'failed': True, 'msg': 'Please specify a name when state is present.'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create'}
+
+ # Create it here
+ api_rval = ocobj.create(params['files'], params['content'])
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = ocobj.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # Remove files
+ if params['files'] and params['delete_after']:
+ Utils.cleanup(params['files'])
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ # if a file path is passed, use it.
+ update = ocobj.needs_update(params['files'], params['content'])
+ if not isinstance(update, bool):
+ return {'failed': True, 'msg': update}
+
+ # No changes
+ if not update:
+ if params['files'] and params['delete_after']:
+ Utils.cleanup(params['files'])
+
+ return {'changed': False, 'results': api_rval['results'][0], 'state': state}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = ocobj.update(params['files'],
+ params['content'],
+ params['force'])
+
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = ocobj.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
diff --git a/roles/lib_openshift/src/class/oc_objectvalidator.py b/roles/lib_openshift/src/class/oc_objectvalidator.py
new file mode 100644
index 000000000..c9fd3b532
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_objectvalidator.py
@@ -0,0 +1,88 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class OCObjectValidator(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ def __init__(self, kubeconfig):
+ ''' Constructor for OCObjectValidator '''
+ # namespace has no meaning for object validation, hardcode to 'default'
+ super(OCObjectValidator, self).__init__('default', kubeconfig)
+
+ def get_invalid(self, kind, invalid_filter):
+ ''' return invalid object information '''
+
+ rval = self._get(kind)
+ if rval['returncode'] != 0:
+ return False, rval, []
+
+ return True, rval, list(filter(invalid_filter, rval['results'][0]['items'])) # wrap filter with list for py3
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params):
+ ''' run the idempotent ansible code
+
+ params comes from the ansible portion of this module
+ '''
+
+ objectvalidator = OCObjectValidator(params['kubeconfig'])
+ all_invalid = {}
+ failed = False
+
+ def _is_invalid_namespace(namespace):
+ # check if it uses a reserved name
+ name = namespace['metadata']['name']
+ if not any((name == 'kube',
+ name == 'kubernetes',
+ name == 'openshift',
+ name.startswith('kube-'),
+ name.startswith('kubernetes-'),
+ name.startswith('openshift-'),)):
+ return False
+
+ # determine if the namespace was created by a user
+ if 'annotations' not in namespace['metadata']:
+ return False
+ return 'openshift.io/requester' in namespace['metadata']['annotations']
+
+ checks = (
+ (
+ 'hostsubnet',
+ lambda x: x['metadata']['name'] != x['host'],
+ u'hostsubnets where metadata.name != host',
+ ),
+ (
+ 'netnamespace',
+ lambda x: x['metadata']['name'] != x['netname'],
+ u'netnamespaces where metadata.name != netname',
+ ),
+ (
+ 'namespace',
+ _is_invalid_namespace,
+ u'namespaces that use reserved names and were not created by infrastructure components',
+ ),
+ )
+
+ for resource, invalid_filter, invalid_msg in checks:
+ success, rval, invalid = objectvalidator.get_invalid(resource, invalid_filter)
+ if not success:
+ return {'failed': True, 'msg': 'Failed to GET {}.'.format(resource), 'state': 'list', 'results': rval}
+ if invalid:
+ failed = True
+ all_invalid[invalid_msg] = invalid
+
+ if failed:
+ return {
+ 'failed': True,
+ 'msg': (
+ "All objects are not valid. If you are a supported customer please contact "
+ "Red Hat Support providing the complete output above. If you are not a customer "
+ "please contact users@lists.openshift.redhat.com for assistance."
+ ),
+ 'state': 'list',
+ 'results': all_invalid
+ }
+
+ return {'msg': 'All objects are valid.'}
diff --git a/roles/lib_openshift/src/class/oc_process.py b/roles/lib_openshift/src/class/oc_process.py
new file mode 100644
index 000000000..62a6bd571
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_process.py
@@ -0,0 +1,185 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCProcess(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5. we need 6
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ namespace,
+ tname=None,
+ params=None,
+ create=False,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ tdata=None,
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCProcess, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = tname
+ self.data = tdata
+ self.params = params
+ self.create = create
+ self._template = None
+
+ @property
+ def template(self):
+ '''template property'''
+ if self._template is None:
+ results = self._process(self.name, False, self.params, self.data)
+ if results['returncode'] != 0:
+ raise OpenShiftCLIError('Error processing template [%s]: %s' %(self.name, results))
+ self._template = results['results']['items']
+
+ return self._template
+
+ def get(self):
+ '''get the template'''
+ results = self._get('template', self.name)
+ if results['returncode'] != 0:
+ # Does the template exist??
+ if 'not found' in results['stderr']:
+ results['returncode'] = 0
+ results['exists'] = False
+ results['results'] = []
+
+ return results
+
+ def delete(self, obj):
+ '''delete a resource'''
+ return self._delete(obj['kind'], obj['metadata']['name'])
+
+ def create_obj(self, obj):
+ '''create a resource'''
+ return self._create_from_content(obj['metadata']['name'], obj)
+
+ def process(self, create=None):
+ '''process a template'''
+ do_create = False
+ if create != None:
+ do_create = create
+ else:
+ do_create = self.create
+
+ return self._process(self.name, do_create, self.params, self.data)
+
+ def exists(self):
+ '''return whether the template exists'''
+ # Always return true if we're being passed template data
+ if self.data:
+ return True
+ t_results = self._get('template', self.name)
+
+ if t_results['returncode'] != 0:
+ # Does the template exist??
+ if 'not found' in t_results['stderr']:
+ return False
+ else:
+ raise OpenShiftCLIError('Something went wrong. %s' % t_results)
+
+ return True
+
+ def needs_update(self):
+ '''attempt to process the template and return it for comparison with oc objects'''
+ obj_results = []
+ for obj in self.template:
+
+ # build a list of types to skip
+ skip = []
+
+ if obj['kind'] == 'ServiceAccount':
+ skip.extend(['secrets', 'imagePullSecrets'])
+ if obj['kind'] == 'BuildConfig':
+ skip.extend(['lastTriggeredImageID'])
+ if obj['kind'] == 'ImageStream':
+ skip.extend(['generation'])
+ if obj['kind'] == 'DeploymentConfig':
+ skip.extend(['lastTriggeredImage'])
+
+ # fetch the current object
+ curr_obj_results = self._get(obj['kind'], obj['metadata']['name'])
+ if curr_obj_results['returncode'] != 0:
+ # Does the template exist??
+ if 'not found' in curr_obj_results['stderr']:
+ obj_results.append((obj, True))
+ continue
+
+ # check the generated object against the existing object
+ if not Utils.check_def_equal(obj, curr_obj_results['results'][0], skip_keys=skip):
+ obj_results.append((obj, True))
+ continue
+
+ obj_results.append((obj, False))
+
+ return obj_results
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ ocprocess = OCProcess(params['namespace'],
+ params['template_name'],
+ params['params'],
+ params['create'],
+ kubeconfig=params['kubeconfig'],
+ tdata=params['content'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocprocess.get()
+
+ if state == 'list':
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg" : api_rval}
+
+ return {"changed" : False, "results": api_rval, "state": state}
+
+ elif state == 'present':
+ if check_mode and params['create']:
+ return {"changed": True, 'msg': "CHECK_MODE: Would have processed template."}
+
+ if not ocprocess.exists() or not params['reconcile']:
+ #FIXME: this code will never get run in a way that succeeds when
+ # module.params['reconcile'] is true. Because oc_process doesn't
+ # create the actual template, the check of ocprocess.exists()
+ # is meaningless. Either it's already here and this code
+ # won't be run, or this code will fail because there is no
+ # template available for oc process to use. Have we conflated
+ # the template's existence with the existence of the objects
+ # it describes?
+
+ # Create it here
+ api_rval = ocprocess.process()
+ if api_rval['returncode'] != 0:
+ return {"failed": True, "msg": api_rval}
+
+ if params['create']:
+ return {"changed": True, "results": api_rval, "state": state}
+
+ return {"changed": False, "results": api_rval, "state": state}
+
+ # verify results
+ update = False
+ rval = []
+ all_results = ocprocess.needs_update()
+ for obj, status in all_results:
+ if status:
+ ocprocess.delete(obj)
+ results = ocprocess.create_obj(obj)
+ results['kind'] = obj['kind']
+ rval.append(results)
+ update = True
+
+ if not update:
+ return {"changed": update, "results": api_rval, "state": state}
+
+ for cmd in rval:
+ if cmd['returncode'] != 0:
+ return {"failed": True, "changed": update, "msg": rval, "state": state}
+
+ return {"changed": update, "results": rval, "state": state}
+
diff --git a/roles/lib_openshift/src/class/oc_project.py b/roles/lib_openshift/src/class/oc_project.py
new file mode 100644
index 000000000..298597067
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_project.py
@@ -0,0 +1,195 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCProject(OpenShiftCLI):
+ ''' Project Class to manage project/namespace objects'''
+ kind = 'namespace'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCProject '''
+ super(OCProject, self).__init__(None, config.kubeconfig)
+ self.config = config
+ self._project = None
+
+ @property
+ def project(self):
+ ''' property for project'''
+ if not self._project:
+ self.get()
+ return self._project
+
+ @project.setter
+ def project(self, data):
+ ''' setter function for project propeorty'''
+ self._project = data
+
+ def exists(self):
+ ''' return whether a project exists '''
+ if self.project:
+ return True
+
+ return False
+
+ def get(self):
+ '''return project '''
+ result = self._get(self.kind, self.config.name)
+
+ if result['returncode'] == 0:
+ self.project = Project(content=result['results'][0])
+ result['results'] = self.project.yaml_dict
+
+ elif 'namespaces "%s" not found' % self.config.name in result['stderr']:
+ result = {'results': [], 'returncode': 0}
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create a project '''
+ cmd = ['new-project', self.config.name]
+ cmd.extend(self.config.to_option_list())
+
+ return self.openshift_cmd(cmd, oadm=True)
+
+ def update(self):
+ '''update a project '''
+
+ if self.config.config_options['display_name']['value'] is not None:
+ self.project.update_annotation('display-name', self.config.config_options['display_name']['value'])
+
+ if self.config.config_options['description']['value'] is not None:
+ self.project.update_annotation('description', self.config.config_options['description']['value'])
+
+ # work around for immutable project field
+ if self.config.config_options['node_selector']['value'] is not None:
+ self.project.update_annotation('node-selector', self.config.config_options['node_selector']['value'])
+
+ return self._replace_content(self.kind, self.config.name, self.project.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ if self.config.config_options['display_name']['value'] is not None:
+ result = self.project.find_annotation("display-name")
+ if result != self.config.config_options['display_name']['value']:
+ return True
+
+ if self.config.config_options['description']['value'] is not None:
+ result = self.project.find_annotation("description")
+ if result != self.config.config_options['description']['value']:
+ return True
+
+ if self.config.config_options['node_selector']['value'] is not None:
+ result = self.project.find_annotation("node-selector")
+ if result != self.config.config_options['node_selector']['value']:
+ return True
+
+ return False
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+
+ node_selector = None
+ if params['node_selector'] is not None:
+ node_selector = ','.join(params['node_selector'])
+
+ pconfig = ProjectConfig(
+ params['name'],
+ 'None',
+ params['kubeconfig'],
+ {
+ 'admin': {'value': params['admin'], 'include': True},
+ 'admin_role': {'value': params['admin_role'], 'include': True},
+ 'description': {'value': params['description'], 'include': True},
+ 'display_name': {'value': params['display_name'], 'include': True},
+ 'node_selector': {'value': node_selector, 'include': True},
+ },
+ )
+
+ oadm_project = OCProject(pconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oadm_project.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oadm_project.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oadm_project.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oadm_project.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oadm_project.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oadm_project.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oadm_project.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
+
+ api_rval = oadm_project.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oadm_project.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. [%s]' % state}
diff --git a/roles/lib_openshift/src/class/oc_pvc.py b/roles/lib_openshift/src/class/oc_pvc.py
new file mode 100644
index 000000000..6b566c301
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_pvc.py
@@ -0,0 +1,169 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCPVC(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'pvc'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCPVC, self).__init__(config.namespace, config.kubeconfig)
+ self.config = config
+ self.namespace = config.namespace
+ self._pvc = None
+
+ @property
+ def pvc(self):
+ ''' property function pvc'''
+ if not self._pvc:
+ self.get()
+ return self._pvc
+
+ @pvc.setter
+ def pvc(self, data):
+ ''' setter function for yedit var '''
+ self._pvc = data
+
+ def bound(self):
+ '''return whether the pvc is bound'''
+ if self.pvc.get_volume_name():
+ return True
+
+ return False
+
+ def exists(self):
+ ''' return whether a pvc exists '''
+ if self.pvc:
+ return True
+
+ return False
+
+ def get(self):
+ '''return pvc information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.pvc = PersistentVolumeClaim(content=result['results'][0])
+ elif '\"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ # need to update the tls information and the service name
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ if self.pvc.get_volume_name() or self.pvc.is_bound():
+ return False
+
+ skip = []
+ return not Utils.check_def_equal(self.config.data, self.pvc.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''run the idempotent ansible code'''
+ pconfig = PersistentVolumeClaimConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['access_modes'],
+ params['volume_capacity'],
+ params['selector'],
+ params['storage_class_name'],
+ )
+ oc_pvc = OCPVC(pconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_pvc.get()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_pvc.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_pvc.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_pvc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_pvc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_pvc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_pvc.pvc.is_bound() or oc_pvc.pvc.get_volume_name():
+ api_rval['msg'] = '##### - This volume is currently bound. Will not update - ####'
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ if oc_pvc.needs_update():
+ api_rval = oc_pvc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_pvc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
diff --git a/roles/lib_openshift/src/class/oc_route.py b/roles/lib_openshift/src/class/oc_route.py
new file mode 100644
index 000000000..dc2f7977b
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_route.py
@@ -0,0 +1,208 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCRoute(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'route'
+
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCRoute, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self._route = None
+
+ @property
+ def route(self):
+ ''' property function for route'''
+ if not self._route:
+ self.get()
+ return self._route
+
+ @route.setter
+ def route(self, data):
+ ''' setter function for route '''
+ self._route = data
+
+ def exists(self):
+ ''' return whether a route exists '''
+ if self.route:
+ return True
+
+ return False
+
+ def get(self):
+ '''return route information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.route = Route(content=result['results'][0])
+ elif 'routes \"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ return self._replace_content(self.kind,
+ self.config.name,
+ self.config.data,
+ force=(self.config.host != self.route.get_host()))
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = []
+ return not Utils.check_def_equal(self.config.data, self.route.yaml_dict, skip_keys=skip, debug=self.verbose)
+
+ @staticmethod
+ def get_cert_data(path, content):
+ '''get the data for a particular value'''
+ rval = None
+ if path and os.path.exists(path) and os.access(path, os.R_OK):
+ rval = open(path).read()
+ elif content:
+ rval = content
+
+ return rval
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent asnible code
+
+ params comes from the ansible portion for this module
+ files: a dictionary for the certificates
+ {'cert': {'path': '',
+ 'content': '',
+ 'value': ''
+ }
+ }
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+ files = {'destcacert': {'path': params['dest_cacert_path'],
+ 'content': params['dest_cacert_content'],
+ 'value': None, },
+ 'cacert': {'path': params['cacert_path'],
+ 'content': params['cacert_content'],
+ 'value': None, },
+ 'cert': {'path': params['cert_path'],
+ 'content': params['cert_content'],
+ 'value': None, },
+ 'key': {'path': params['key_path'],
+ 'content': params['key_content'],
+ 'value': None, }, }
+
+ if params['tls_termination'] and params['tls_termination'].lower() != 'passthrough': # E501
+
+ for key, option in files.items():
+ if not option['path'] and not option['content']:
+ continue
+
+ option['value'] = OCRoute.get_cert_data(option['path'], option['content']) # E501
+
+ if not option['value']:
+ return {'failed': True,
+ 'msg': 'Verify that you pass a correct value for %s' % key}
+
+ rconfig = RouteConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['labels'],
+ files['destcacert']['value'],
+ files['cacert']['value'],
+ files['cert']['value'],
+ files['key']['value'],
+ params['host'],
+ params['tls_termination'],
+ params['service_name'],
+ params['wildcard_policy'],
+ params['weight'],
+ params['port'])
+
+ oc_route = OCRoute(rconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_route.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False,
+ 'results': api_rval['results'],
+ 'state': 'list'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_route.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'} # noqa: E501
+
+ api_rval = oc_route.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"} # noqa: E501
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_route.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} # noqa: E501
+
+ # Create it here
+ api_rval = oc_route.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ # return the created object
+ api_rval = oc_route.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ return {'changed': True, 'results': api_rval, 'state': "present"} # noqa: E501
+
+ ########
+ # Update
+ ########
+ if oc_route.needs_update():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'} # noqa: E501
+
+ api_rval = oc_route.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ # return the created object
+ api_rval = oc_route.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
+
+ return {'changed': True, 'results': api_rval, 'state': "present"} # noqa: E501
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+ # catch all
+ return {'failed': True, 'msg': "Unknown State passed"}
diff --git a/roles/lib_openshift/src/class/oc_scale.py b/roles/lib_openshift/src/class/oc_scale.py
new file mode 100644
index 000000000..6c3ceb8cf
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_scale.py
@@ -0,0 +1,108 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class OCScale(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ resource_name,
+ namespace,
+ replicas,
+ kind,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCScale '''
+ super(OCScale, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.kind = kind
+ self.replicas = replicas
+ self.name = resource_name
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var '''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var '''
+ self._resource = data
+
+ def get(self):
+ '''return replicas information '''
+ vol = self._get(self.kind, self.name)
+ if vol['returncode'] == 0:
+ if self.kind == 'dc':
+ # The resource returned from a query could be an rc or dc.
+ # pylint: disable=redefined-variable-type
+ self.resource = DeploymentConfig(content=vol['results'][0])
+ vol['results'] = [self.resource.get_replicas()]
+ if self.kind == 'rc':
+ # The resource returned from a query could be an rc or dc.
+ # pylint: disable=redefined-variable-type
+ self.resource = ReplicationController(content=vol['results'][0])
+ vol['results'] = [self.resource.get_replicas()]
+
+ return vol
+
+ def put(self):
+ '''update replicas into dc '''
+ self.resource.update_replicas(self.replicas)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def needs_update(self):
+ ''' verify whether an update is needed '''
+ return self.resource.needs_update_replicas(self.replicas)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''perform the idempotent ansible logic'''
+
+ oc_scale = OCScale(params['name'],
+ params['namespace'],
+ params['replicas'],
+ params['kind'],
+ params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_scale.get()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'result': api_rval['results'], 'state': 'list'} # noqa: E501
+
+ elif state == 'present':
+ ########
+ # Update
+ ########
+ if oc_scale.needs_update():
+ if check_mode:
+ return {'changed': True, 'result': 'CHECK_MODE: Would have updated.'} # noqa: E501
+ api_rval = oc_scale.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_scale.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
+
+ return {'changed': False, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
+
+ return {'failed': True, 'msg': 'Unknown state passed. [{}]'.format(state)}
diff --git a/roles/lib_openshift/src/class/oc_secret.py b/roles/lib_openshift/src/class/oc_secret.py
new file mode 100644
index 000000000..5322d6241
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_secret.py
@@ -0,0 +1,209 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: skip-file
+
+# pylint: disable=wrong-import-position,wrong-import-order
+import base64
+
+# pylint: disable=too-many-arguments
+class OCSecret(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools
+ '''
+ def __init__(self,
+ namespace,
+ secret_name=None,
+ secret_type=None,
+ decode=False,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCSecret, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
+ self.name = secret_name
+ self.type = secret_type
+ self.decode = decode
+
+ def get(self):
+ '''return a secret by name '''
+ results = self._get('secrets', self.name)
+ results['decoded'] = {}
+ results['exists'] = False
+ if results['returncode'] == 0 and results['results'][0]:
+ results['exists'] = True
+ if self.decode:
+ if 'data' in results['results'][0]:
+ for sname, value in results['results'][0]['data'].items():
+ results['decoded'][sname] = base64.b64decode(value)
+
+ if results['returncode'] != 0 and '"%s" not found' % self.name in results['stderr']:
+ results['returncode'] = 0
+
+ return results
+
+ def delete(self):
+ '''delete a secret by name'''
+ return self._delete('secrets', self.name)
+
+ def create(self, files=None, contents=None, force=False):
+ '''Create a secret '''
+ if not files:
+ files = Utils.create_tmp_files_from_contents(contents)
+
+ secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files]
+ cmd = ['secrets', 'new', self.name]
+ if self.type is not None:
+ cmd.append("--type=%s" % (self.type))
+ if force:
+ cmd.append('--confirm')
+ cmd.extend(secrets)
+
+ results = self.openshift_cmd(cmd)
+
+ return results
+
+ def update(self, files, force=False):
+ '''run update secret
+
+ This receives a list of file names and converts it into a secret.
+ The secret is then written to disk and passed into the `oc replace` command.
+ '''
+ secret = self.prep_secret(files, force)
+ if secret['returncode'] != 0:
+ return secret
+
+ sfile_path = '/tmp/%s' % self.name
+ with open(sfile_path, 'w') as sfd:
+ sfd.write(json.dumps(secret['results']))
+
+ atexit.register(Utils.cleanup, [sfile_path])
+
+ return self._replace(sfile_path, force=force)
+
+ def prep_secret(self, files=None, contents=None, force=False):
+ ''' return what the secret would look like if created
+ This is accomplished by passing -ojson. This will most likely change in the future
+ '''
+ if not files:
+ files = Utils.create_tmp_files_from_contents(contents)
+
+ secrets = ["%s=%s" % (sfile['name'], sfile['path']) for sfile in files]
+ cmd = ['-ojson', 'secrets', 'new', self.name]
+ if self.type is not None:
+ cmd.extend(["--type=%s" % (self.type)])
+ if force:
+ cmd.append('--confirm')
+ cmd.extend(secrets)
+
+ return self.openshift_cmd(cmd, output=True)
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ ocsecret = OCSecret(params['namespace'],
+ params['name'],
+ params['type'],
+ params['decode'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = ocsecret.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, state: 'list'}
+
+ if not params['name']:
+ return {'failed': True,
+ 'msg': 'Please specify a name when state is absent|present.'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if not Utils.exists(api_rval['results'], params['name']):
+ return {'changed': False, 'state': 'absent'}
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a delete.'}
+
+ api_rval = ocsecret.delete()
+ return {'changed': True, 'results': api_rval, 'state': 'absent'}
+
+ if state == 'present':
+ if params['files']:
+ files = params['files']
+ elif params['contents']:
+ files = Utils.create_tmp_files_from_contents(params['contents'])
+ else:
+ files = [{'name': 'null', 'path': os.devnull}]
+
+ ########
+ # Create
+ ########
+ if not Utils.exists(api_rval['results'], params['name']):
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'Would have performed a create.'}
+
+ api_rval = ocsecret.create(files, params['contents'], force=params['force'])
+
+ # Remove files
+ if files and params['delete_after']:
+ Utils.cleanup([ftmp['path'] for ftmp in files])
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True,
+ 'msg': api_rval}
+
+ return {'changed': True,
+ 'results': api_rval,
+ 'state': 'present'}
+
+ ########
+ # Update
+ ########
+ secret = ocsecret.prep_secret(params['files'], params['contents'], force=params['force'])
+
+ if secret['returncode'] != 0:
+ return {'failed': True, 'msg': secret}
+
+ if Utils.check_def_equal(secret['results'], api_rval['results'][0]):
+
+ # Remove files
+ if files and params['delete_after']:
+ Utils.cleanup([ftmp['path'] for ftmp in files])
+
+ return {'changed': False,
+ 'results': secret['results'],
+ 'state': 'present'}
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'Would have performed an update.'}
+
+ api_rval = ocsecret.update(files, force=params['force'])
+
+ # Remove files
+ if secret and params['delete_after']:
+ Utils.cleanup([ftmp['path'] for ftmp in files])
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True,
+ 'msg': api_rval}
+
+ return {'changed': True,
+ 'results': api_rval,
+ 'state': 'present'}
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
diff --git a/roles/lib_openshift/src/class/oc_service.py b/roles/lib_openshift/src/class/oc_service.py
new file mode 100644
index 000000000..7268a0c88
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_service.py
@@ -0,0 +1,175 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCService(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'service'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ labels,
+ selector,
+ cluster_ip,
+ portal_ip,
+ ports,
+ session_affinity,
+ service_type,
+ external_ips,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCService, self).__init__(namespace, kubeconfig, verbose)
+ self.namespace = namespace
+ self.config = ServiceConfig(sname, namespace, ports, selector, labels,
+ cluster_ip, portal_ip, session_affinity, service_type,
+ external_ips)
+ self.user_svc = Service(content=self.config.data)
+ self.svc = None
+
+ @property
+ def service(self):
+ ''' property function service'''
+ if not self.svc:
+ self.get()
+ return self.svc
+
+ @service.setter
+ def service(self, data):
+ ''' setter function for service var '''
+ self.svc = data
+
+ def exists(self):
+ ''' return whether a service exists '''
+ if self.service:
+ return True
+
+ return False
+
+ def get(self):
+ '''return service information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.service = Service(content=result['results'][0])
+ result['clusterip'] = self.service.get('spec.clusterIP')
+ elif 'services \"%s\" not found' % self.config.name in result['stderr']:
+ result['clusterip'] = ''
+ result['returncode'] = 0
+
+ return result
+
+ def delete(self):
+ '''delete the service'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create a service '''
+ return self._create_from_content(self.config.name, self.user_svc.yaml_dict)
+
+ def update(self):
+ '''create a service '''
+ # Need to copy over the portalIP and the serviceIP settings
+
+ self.user_svc.add_cluster_ip(self.service.get('spec.clusterIP'))
+ self.user_svc.add_portal_ip(self.service.get('spec.portalIP'))
+ return self._replace_content(self.kind, self.config.name, self.user_svc.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = ['clusterIP', 'portalIP']
+ return not Utils.check_def_equal(self.user_svc.yaml_dict, self.service.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-return-statements,too-many-branches
+ @staticmethod
+ def run_ansible(params, check_mode):
+ '''Run the idempotent ansible code'''
+ oc_svc = OCService(params['name'],
+ params['namespace'],
+ params['labels'],
+ params['selector'],
+ params['clusterip'],
+ params['portalip'],
+ params['ports'],
+ params['session_affinity'],
+ params['service_type'],
+ params['external_ips'],
+ params['kubeconfig'],
+ params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_svc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_svc.exists():
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'CHECK_MODE: Would have performed a delete.'} # noqa: E501
+
+ api_rval = oc_svc.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_svc.exists():
+
+ if check_mode:
+ return {'changed': True,
+ 'msg': 'CHECK_MODE: Would have performed a create.'} # noqa: E501
+
+ # Create it here
+ api_rval = oc_svc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_svc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_svc.needs_update():
+ api_rval = oc_svc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_svc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'results': api_rval, 'state': state}
+
+ return {'failed': True, 'msg': 'UNKNOWN state passed. [%s]' % state}
diff --git a/roles/lib_openshift/src/class/oc_serviceaccount.py b/roles/lib_openshift/src/class/oc_serviceaccount.py
new file mode 100644
index 000000000..d6777afc1
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_serviceaccount.py
@@ -0,0 +1,164 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class OCServiceAccount(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'sa'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCServiceAccount, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self.service_account = None
+
+ def exists(self):
+ ''' return whether a volume exists '''
+ if self.service_account:
+ return True
+
+ return False
+
+ def get(self):
+ '''return volume information '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.service_account = ServiceAccount(content=result['results'][0])
+ elif '\"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ # need to update the tls information and the service name
+ for secret in self.config.secrets:
+ result = self.service_account.find_secret(secret)
+ if not result:
+ self.service_account.add_secret(secret)
+
+ for secret in self.config.image_pull_secrets:
+ result = self.service_account.find_image_pull_secret(secret)
+ if not result:
+ self.service_account.add_image_pull_secret(secret)
+
+ return self._replace_content(self.kind, self.config.name, self.config.data)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ # since creating an service account generates secrets and imagepullsecrets
+ # check_def_equal will not work
+ # Instead, verify all secrets passed are in the list
+ for secret in self.config.secrets:
+ result = self.service_account.find_secret(secret)
+ if not result:
+ return True
+
+ for secret in self.config.image_pull_secrets:
+ result = self.service_account.find_image_pull_secret(secret)
+ if not result:
+ return True
+
+ return False
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ rconfig = ServiceAccountConfig(params['name'],
+ params['namespace'],
+ params['kubeconfig'],
+ params['secrets'],
+ params['image_pull_secrets'],
+ )
+
+ oc_sa = OCServiceAccount(rconfig,
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_sa.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': 'list'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_sa.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_sa.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': 'absent'}
+
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_sa.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_sa.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sa.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ ########
+ # Update
+ ########
+ if oc_sa.needs_update():
+ api_rval = oc_sa.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sa.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ return {'changed': False, 'results': api_rval, 'state': 'present'}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
diff --git a/roles/lib_openshift/src/class/oc_serviceaccount_secret.py b/roles/lib_openshift/src/class/oc_serviceaccount_secret.py
new file mode 100644
index 000000000..4f1c8c926
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_serviceaccount_secret.py
@@ -0,0 +1,138 @@
+# pylint: skip-file
+# flake8: noqa
+
+class OCServiceAccountSecret(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+
+ kind = 'sa'
+ def __init__(self, config, verbose=False):
+ ''' Constructor for OpenshiftOC '''
+ super(OCServiceAccountSecret, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self.verbose = verbose
+ self._service_account = None
+
+ @property
+ def service_account(self):
+ ''' Property for the service account '''
+ if not self._service_account:
+ self.get()
+ return self._service_account
+
+ @service_account.setter
+ def service_account(self, data):
+ ''' setter for the service account '''
+ self._service_account = data
+
+ def exists(self, in_secret):
+ ''' verifies if secret exists in the service account '''
+ result = self.service_account.find_secret(in_secret)
+ if not result:
+ return False
+ return True
+
+ def get(self):
+ ''' get the service account definition from the master '''
+ sao = self._get(OCServiceAccountSecret.kind, self.config.name)
+ if sao['returncode'] == 0:
+ self.service_account = ServiceAccount(content=sao['results'][0])
+ sao['results'] = self.service_account.get('secrets')
+ return sao
+
+ def delete(self):
+ ''' delete secrets '''
+
+ modified = []
+ for rem_secret in self.config.secrets:
+ modified.append(self.service_account.delete_secret(rem_secret))
+
+ if any(modified):
+ return self._replace_content(OCServiceAccountSecret.kind, self.config.name, self.service_account.yaml_dict)
+
+ return {'returncode': 0, 'changed': False}
+
+ def put(self):
+ ''' place secrets into sa '''
+ modified = False
+ for add_secret in self.config.secrets:
+ if not self.service_account.find_secret(add_secret):
+ self.service_account.add_secret(add_secret)
+ modified = True
+
+ if modified:
+ return self._replace_content(OCServiceAccountSecret.kind, self.config.name, self.service_account.yaml_dict)
+
+ return {'returncode': 0, 'changed': False}
+
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ ''' run the ansible idempotent code '''
+
+ sconfig = ServiceAccountConfig(params['service_account'],
+ params['namespace'],
+ params['kubeconfig'],
+ [params['secret']],
+ None)
+
+ oc_sa_sec = OCServiceAccountSecret(sconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_sa_sec.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_sa_sec.exists(params['secret']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have removed the " + \
+ "secret from the service account.'}
+
+ api_rval = oc_sa_sec.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_sa_sec.exists(params['secret']):
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have added the ' + \
+ 'secret to the service account.'}
+
+ # Create it here
+ api_rval = oc_sa_sec.put()
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sa_sec.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
diff --git a/roles/lib_openshift/src/class/oc_storageclass.py b/roles/lib_openshift/src/class/oc_storageclass.py
new file mode 100644
index 000000000..aced586ae
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_storageclass.py
@@ -0,0 +1,155 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class OCStorageClass(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'storageclass'
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ verbose=False):
+ ''' Constructor for OCStorageClass '''
+ super(OCStorageClass, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose)
+ self.config = config
+ self.storage_class = None
+
+ def exists(self):
+ ''' return whether a storageclass exists'''
+ if self.storage_class:
+ return True
+
+ return False
+
+ def get(self):
+ '''return storageclass '''
+ result = self._get(self.kind, self.config.name)
+ if result['returncode'] == 0:
+ self.storage_class = StorageClass(content=result['results'][0])
+ elif '\"%s\" not found' % self.config.name in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ '''delete the object'''
+ return self._delete(self.kind, self.config.name)
+
+ def create(self):
+ '''create the object'''
+ return self._create_from_content(self.config.name, self.config.data)
+
+ def update(self):
+ '''update the object'''
+ # parameters are currently unable to be updated. need to delete and recreate
+ self.delete()
+ # pause here and attempt to wait for delete.
+ # Better option would be to poll
+ import time
+ time.sleep(5)
+ return self.create()
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ # check if params have updated
+ if self.storage_class.get_parameters() != self.config.parameters:
+ return True
+
+ for anno_key, anno_value in self.storage_class.get_annotations().items():
+ if 'is-default-class' in anno_key and anno_value != self.config.default_storage_class:
+ return True
+
+ return False
+
+ @staticmethod
+ # pylint: disable=too-many-return-statements,too-many-branches
+ # TODO: This function should be refactored into its individual parts.
+ def run_ansible(params, check_mode):
+ '''run the ansible idempotent code'''
+
+ rconfig = StorageClassConfig(params['name'],
+ provisioner="kubernetes.io/{}".format(params['provisioner']),
+ parameters=params['parameters'],
+ annotations=params['annotations'],
+ api_version="storage.k8s.io/{}".format(params['api_version']),
+ default_storage_class=params.get('default_storage_class', 'false'),
+ kubeconfig=params['kubeconfig'],
+ )
+
+ oc_sc = OCStorageClass(rconfig, verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_sc.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': 'list'}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_sc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_sc.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': 'absent'}
+
+ return {'changed': False, 'state': 'absent'}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_sc.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_sc.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ ########
+ # Update
+ ########
+ if oc_sc.needs_update():
+ api_rval = oc_sc.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_sc.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': 'present'}
+
+ return {'changed': False, 'results': api_rval, 'state': 'present'}
+
+
+ return {'failed': True,
+ 'changed': False,
+ 'msg': 'Unknown state passed. %s' % state,
+ 'state': 'unknown'}
diff --git a/roles/lib_openshift/src/class/oc_user.py b/roles/lib_openshift/src/class/oc_user.py
new file mode 100644
index 000000000..d9e4eac13
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_user.py
@@ -0,0 +1,227 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class OCUser(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'users'
+
+ def __init__(self,
+ config,
+ groups=None,
+ verbose=False):
+ ''' Constructor for OCUser '''
+ # namespace has no meaning for user operations, hardcode to 'default'
+ super(OCUser, self).__init__('default', config.kubeconfig)
+ self.config = config
+ self.groups = groups
+ self._user = None
+
+ @property
+ def user(self):
+ ''' property function user'''
+ if not self._user:
+ self.get()
+ return self._user
+
+ @user.setter
+ def user(self, data):
+ ''' setter function for user '''
+ self._user = data
+
+ def exists(self):
+ ''' return whether a user exists '''
+ if self.user:
+ return True
+
+ return False
+
+ def get(self):
+ ''' return user information '''
+ result = self._get(self.kind, self.config.username)
+ if result['returncode'] == 0:
+ self.user = User(content=result['results'][0])
+ elif 'users \"%s\" not found' % self.config.username in result['stderr']:
+ result['returncode'] = 0
+ result['results'] = [{}]
+
+ return result
+
+ def delete(self):
+ ''' delete the object '''
+ return self._delete(self.kind, self.config.username)
+
+ def create_group_entries(self):
+ ''' make entries for user to the provided group list '''
+ if self.groups != None:
+ for group in self.groups:
+ cmd = ['groups', 'add-users', group, self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+
+ return rval
+
+ return {'returncode': 0}
+
+ def create(self):
+ ''' create the object '''
+ rval = self.create_group_entries()
+ if rval['returncode'] != 0:
+ return rval
+
+ return self._create_from_content(self.config.username, self.config.data)
+
+ def group_update(self):
+ ''' update group membership '''
+ rval = {'returncode': 0}
+ cmd = ['get', 'groups', '-o', 'json']
+ all_groups = self.openshift_cmd(cmd, output=True)
+
+ # pylint misindentifying all_groups['results']['items'] type
+ # pylint: disable=invalid-sequence-index
+ for group in all_groups['results']['items']:
+ # If we're supposed to be in this group
+ if group['metadata']['name'] in self.groups \
+ and (group['users'] is None or self.config.username not in group['users']):
+ cmd = ['groups', 'add-users', group['metadata']['name'],
+ self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+ # else if we're in the group, but aren't supposed to be
+ elif group['users'] != None and self.config.username in group['users'] \
+ and group['metadata']['name'] not in self.groups:
+ cmd = ['groups', 'remove-users', group['metadata']['name'],
+ self.config.username]
+ rval = self.openshift_cmd(cmd, oadm=True)
+ if rval['returncode'] != 0:
+ return rval
+
+ return rval
+
+ def update(self):
+ ''' update the object '''
+ rval = self.group_update()
+ if rval['returncode'] != 0:
+ return rval
+
+ # need to update the user's info
+ return self._replace_content(self.kind, self.config.username, self.config.data, force=True)
+
+ def needs_group_update(self):
+ ''' check if there are group membership changes '''
+ cmd = ['get', 'groups', '-o', 'json']
+ all_groups = self.openshift_cmd(cmd, output=True)
+
+ # pylint misindentifying all_groups['results']['items'] type
+ # pylint: disable=invalid-sequence-index
+ for group in all_groups['results']['items']:
+ # If we're supposed to be in this group
+ if group['metadata']['name'] in self.groups \
+ and (group['users'] is None or self.config.username not in group['users']):
+ return True
+ # else if we're in the group, but aren't supposed to be
+ elif group['users'] != None and self.config.username in group['users'] \
+ and group['metadata']['name'] not in self.groups:
+ return True
+
+ return False
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ skip = []
+ if self.needs_group_update():
+ return True
+
+ return not Utils.check_def_equal(self.config.data, self.user.yaml_dict, skip_keys=skip, debug=True)
+
+ # pylint: disable=too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ ''' run the idempotent ansible code
+
+ params comes from the ansible portion of this module
+ check_mode: does the module support check mode. (module.check_mode)
+ '''
+
+ uconfig = UserConfig(params['kubeconfig'],
+ params['username'],
+ params['full_name'],
+ )
+
+ oc_user = OCUser(uconfig, params['groups'],
+ verbose=params['debug'])
+ state = params['state']
+
+ api_rval = oc_user.get()
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': "list"}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_user.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a delete.'}
+
+ api_rval = oc_user.delete()
+
+ return {'changed': True, 'results': api_rval, 'state': "absent"}
+ return {'changed': False, 'state': "absent"}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_user.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_user.create()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_user.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ ########
+ # Update
+ ########
+ if oc_user.needs_update():
+ api_rval = oc_user.update()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ orig_cmd = api_rval['cmd']
+ # return the created object
+ api_rval = oc_user.get()
+ # overwrite the get/list cmd
+ api_rval['cmd'] = orig_cmd
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': "present"}
+
+ return {'changed': False, 'results': api_rval, 'state': "present"}
+
+ return {'failed': True,
+ 'changed': False,
+ 'results': 'Unknown state passed. %s' % state,
+ 'state': "unknown"}
diff --git a/roles/lib_openshift/src/class/oc_version.py b/roles/lib_openshift/src/class/oc_version.py
new file mode 100644
index 000000000..7f8c721d8
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_version.py
@@ -0,0 +1,47 @@
+# flake8: noqa
+# pylint: skip-file
+
+
+# pylint: disable=too-many-instance-attributes
+class OCVersion(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ config,
+ debug):
+ ''' Constructor for OCVersion '''
+ super(OCVersion, self).__init__(None, config)
+ self.debug = debug
+
+ def get(self):
+ '''get and return version information '''
+
+ results = {}
+
+ version_results = self._version()
+
+ if version_results['returncode'] == 0:
+ filtered_vers = Utils.filter_versions(version_results['results'])
+ custom_vers = Utils.add_custom_versions(filtered_vers)
+
+ results['returncode'] = version_results['returncode']
+ results.update(filtered_vers)
+ results.update(custom_vers)
+
+ return results
+
+ raise OpenShiftCLIError('Problem detecting openshift version.')
+
+ @staticmethod
+ def run_ansible(params):
+ '''run the idempotent ansible code'''
+ oc_version = OCVersion(params['kubeconfig'], params['debug'])
+
+ if params['state'] == 'list':
+
+ #pylint: disable=protected-access
+ result = oc_version.get()
+ return {'state': params['state'],
+ 'results': result,
+ 'changed': False}
diff --git a/roles/lib_openshift/src/class/oc_volume.py b/roles/lib_openshift/src/class/oc_volume.py
new file mode 100644
index 000000000..45b58a516
--- /dev/null
+++ b/roles/lib_openshift/src/class/oc_volume.py
@@ -0,0 +1,195 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class OCVolume(OpenShiftCLI):
+ ''' Class to wrap the oc command line tools '''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ # pylint allows 5
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ kind,
+ resource_name,
+ namespace,
+ vol_name,
+ mount_path,
+ mount_type,
+ secret_name,
+ claim_size,
+ claim_name,
+ configmap_name,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False):
+ ''' Constructor for OCVolume '''
+ super(OCVolume, self).__init__(namespace, kubeconfig)
+ self.kind = kind
+ self.volume_info = {'name': vol_name,
+ 'secret_name': secret_name,
+ 'path': mount_path,
+ 'type': mount_type,
+ 'claimSize': claim_size,
+ 'claimName': claim_name,
+ 'configmap_name': configmap_name}
+ self.volume, self.volume_mount = Volume.create_volume_structure(self.volume_info)
+ self.name = resource_name
+ self.namespace = namespace
+ self.kubeconfig = kubeconfig
+ self.verbose = verbose
+ self._resource = None
+
+ @property
+ def resource(self):
+ ''' property function for resource var '''
+ if not self._resource:
+ self.get()
+ return self._resource
+
+ @resource.setter
+ def resource(self, data):
+ ''' setter function for resource var '''
+ self._resource = data
+
+ def exists(self):
+ ''' return whether a volume exists '''
+ volume_mount_found = False
+ volume_found = self.resource.exists_volume(self.volume)
+ if not self.volume_mount and volume_found:
+ return True
+
+ if self.volume_mount:
+ volume_mount_found = self.resource.exists_volume_mount(self.volume_mount)
+
+ if volume_found and self.volume_mount and volume_mount_found:
+ return True
+
+ return False
+
+ def get(self):
+ '''return volume information '''
+ vol = self._get(self.kind, self.name)
+ if vol['returncode'] == 0:
+ if self.kind == 'dc':
+ self.resource = DeploymentConfig(content=vol['results'][0])
+ vol['results'] = self.resource.get_volumes()
+
+ return vol
+
+ def delete(self):
+ '''remove a volume'''
+ self.resource.delete_volume_by_name(self.volume)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def put(self):
+ '''place volume into dc '''
+ self.resource.update_volume(self.volume)
+ self.resource.get_volumes()
+ self.resource.update_volume_mount(self.volume_mount)
+ return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
+
+ def needs_update(self):
+ ''' verify an update is needed '''
+ return self.resource.needs_update_volume(self.volume, self.volume_mount)
+
+ # pylint: disable=too-many-branches,too-many-return-statements
+ @staticmethod
+ def run_ansible(params, check_mode=False):
+ '''run the idempotent ansible code'''
+ oc_volume = OCVolume(params['kind'],
+ params['name'],
+ params['namespace'],
+ params['vol_name'],
+ params['mount_path'],
+ params['mount_type'],
+ # secrets
+ params['secret_name'],
+ # pvc
+ params['claim_size'],
+ params['claim_name'],
+ # configmap
+ params['configmap_name'],
+ kubeconfig=params['kubeconfig'],
+ verbose=params['debug'])
+
+ state = params['state']
+
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ #####
+ # Get
+ #####
+ if state == 'list':
+ return {'changed': False, 'results': api_rval['results'], 'state': state}
+
+ ########
+ # Delete
+ ########
+ if state == 'absent':
+ if oc_volume.exists():
+
+ if check_mode:
+ return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'}
+
+ api_rval = oc_volume.delete()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ return {'changed': False, 'state': state}
+
+ if state == 'present':
+ ########
+ # Create
+ ########
+ if not oc_volume.exists():
+
+ if check_mode:
+ return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
+
+ # Create it here
+ api_rval = oc_volume.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, 'state': state}
+
+ ########
+ # Update
+ ########
+ if oc_volume.needs_update():
+ api_rval = oc_volume.put()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ # return the created object
+ api_rval = oc_volume.get()
+
+ if api_rval['returncode'] != 0:
+ return {'failed': True, 'msg': api_rval}
+
+ return {'changed': True, 'results': api_rval, state: state}
+
+ return {'changed': False, 'results': api_rval, state: state}
+
+ return {'failed': True, 'msg': 'Unknown state passed. {}'.format(state)}
diff --git a/roles/lib_openshift/src/doc/atomic_container b/roles/lib_openshift/src/doc/atomic_container
new file mode 100644
index 000000000..53fc40f36
--- /dev/null
+++ b/roles/lib_openshift/src/doc/atomic_container
@@ -0,0 +1,36 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_atomic_container
+short_description: Manage the container images on the atomic host platform
+description:
+ - Manage the container images on the atomic host platform
+ - Allows to execute the commands on the container images
+requirements:
+ - atomic
+ - "python >= 2.6"
+options:
+ name:
+ description:
+ - Name of the container
+ required: True
+ default: null
+ image:
+ description:
+ - The image to use to install the container
+ required: True
+ default: null
+ state:
+ description:
+ - State of the container
+ required: True
+ choices: ["latest", "absent", "latest", "rollback"]
+ default: "latest"
+ values:
+ description:
+ - Values for the installation of the container
+ required: False
+ default: None
+'''
diff --git a/roles/lib_openshift/src/doc/ca_server_cert b/roles/lib_openshift/src/doc/ca_server_cert
new file mode 100644
index 000000000..7f2be4ada
--- /dev/null
+++ b/roles/lib_openshift/src/doc/ca_server_cert
@@ -0,0 +1,102 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_ca_server_cert
+short_description: Module to run openshift oc adm ca create-server-cert
+description:
+ - Wrapper around the openshift `oc adm ca create-server-cert` command.
+options:
+ state:
+ description:
+ - Present is the only supported state. The state present means that `oc adm ca` will generate a certificate
+ - and verify if the hostnames and the ClusterIP exists in the certificate.
+ - When create-server-cert is desired then the following parameters are passed.
+ - ['cert', 'key', 'signer_cert', 'signer_key', 'signer_serial']
+ required: false
+ default: present
+ choices:
+ - present
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ cert:
+ description:
+ - The certificate file. Choose a name that indicates what the service is.
+ required: false
+ default: None
+ aliases: []
+ key:
+ description:
+ - The key file. Choose a name that indicates what the service is.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Force updating of the existing cert and key files
+ required: false
+ default: False
+ aliases: []
+ signer_cert:
+ description:
+ - The signer certificate file.
+ required: false
+ default: /etc/origin/master/ca.crt
+ aliases: []
+ signer_key:
+ description:
+ - The signer key file.
+ required: false
+ default: /etc/origin/master/ca.key
+ aliases: []
+ signer_serial:
+ description:
+ - The signer serial file.
+ required: false
+ default: /etc/origin/master/ca.serial.txt
+ aliases: []
+ hostnames:
+ description:
+ - Every hostname or IP that server certs should be valid for
+ required: false
+ default: []
+ aliases: []
+ backup:
+ description:
+ - Whether to backup the cert and key files before writing them.
+ required: false
+ default: True
+ aliases: []
+ expire_days:
+ description
+ - Validity of the certificate in days
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Create a self-signed cert
+ oc_adm_ca_server_cert:
+ signer_cert: /etc/origin/master/ca.crt
+ signer_key: /etc/origin/master/ca.key
+ signer_serial: /etc/origin/master/ca.serial.txt
+ hostnames: "registry.test.openshift.com,127.0.0.1,docker-registry.default.svc.cluster.local"
+ cert: /etc/origin/master/registry.crt
+ key: /etc/origin/master/registry.key
+'''
diff --git a/roles/lib_openshift/src/doc/clusterrole b/roles/lib_openshift/src/doc/clusterrole
new file mode 100644
index 000000000..3d14a2dfb
--- /dev/null
+++ b/roles/lib_openshift/src/doc/clusterrole
@@ -0,0 +1,66 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_clusterrole
+short_description: Modify, and idempotently manage openshift clusterroles
+description:
+ - Manage openshift clusterroles
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a clusterrole
+ - absent - will remove a clusterrole
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ rules:
+ description:
+ - A list of dictionaries that have the rule parameters.
+ - e.g. rules=[{'apiGroups': [""], 'attributeRestrictions': None, 'verbs': ['get'], 'resources': []}]
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: query a list of env vars on dc
+ oc_clusterrole:
+ name: myclusterrole
+ state: list
+
+- name: Set the following variables.
+ oc_clusterrole:
+ name: myclusterrole
+ rules:
+ apiGroups:
+ - ""
+ attributeRestrictions: null
+ verbs: []
+ resources: []
+'''
diff --git a/roles/lib_openshift/src/doc/configmap b/roles/lib_openshift/src/doc/configmap
new file mode 100644
index 000000000..5ca8292c4
--- /dev/null
+++ b/roles/lib_openshift/src/doc/configmap
@@ -0,0 +1,72 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_configmap
+short_description: Modify, and idempotently manage openshift configmaps
+description:
+ - Modify openshift configmaps programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a configmap
+ - absent - will remove the configmap
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: True
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ from_file:
+ description:
+ - A dict of key, value pairs representing the configmap key and the value represents the file path.
+ required: false
+ default: None
+ aliases: []
+ from_literal:
+ description:
+ - A dict of key, value pairs representing the configmap key and the value represents the string content
+ required: false
+ default: None
+ aliases: []
+author:
+- "kenny woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create group
+ oc_configmap:
+ state: present
+ name: testmap
+ from_file:
+ secret: /path/to/secret
+ from_literal:
+ title: systemadmin
+ register: configout
+'''
diff --git a/roles/lib_openshift/src/doc/csr b/roles/lib_openshift/src/doc/csr
new file mode 100644
index 000000000..db72dbda3
--- /dev/null
+++ b/roles/lib_openshift/src/doc/csr
@@ -0,0 +1,80 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_csr
+short_description: Module to approve or deny openshift certificate signing requests
+description:
+ - Wrapper around the openshift `oc adm certificate approve|deny <csr>` command.
+options:
+ state:
+ description:
+ - approve|deny|list Approve, deny, and list are the only supported states for certificates
+ required: false
+ default: present
+ choices:
+ - present
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ nodes:
+ description:
+ - A list of the names of the nodes in which to accept the certificates
+ required: false
+ default: None
+ aliases: []
+ timeout:
+ description:
+ - This flag allows for a timeout value when approving nodes.
+ required: false
+ default: 30
+ aliases: []
+ timeout:
+ description:
+ - This flag allows for a timeout value when doing node approvals.
+ - A zero value for the timeout will block until the nodes have been accepted
+ required: false
+ default: 30
+ aliases: []
+ approve_all:
+ description:
+ - This flag allows for the module to approve all CSRs that are found.
+ - This facilitates testing.
+ required: false
+ default: False
+ aliases: []
+ service_account:
+ description:
+ - This parameter tells the approval process which service account is being used for the requests
+ required: false
+ default: node-bootstrapper
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Approve certificates for node xyz
+ oc_adm_scr:
+ nodes:
+ - xyz
+ timeout: 300
+
+- name: Approve certificates for node xyz
+ oc_adm_scr:
+ nodes:
+ - xyz
+ timeout: 0
+'''
diff --git a/roles/lib_openshift/src/doc/edit b/roles/lib_openshift/src/doc/edit
new file mode 100644
index 000000000..212d88f65
--- /dev/null
+++ b/roles/lib_openshift/src/doc/edit
@@ -0,0 +1,116 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_edit
+short_description: Modify, and idempotently manage openshift objects.
+description:
+ - Modify openshift objects programmatically.
+options:
+ state:
+ description:
+ - Currently present is only supported state.
+ required: true
+ default: present
+ choices: ["present"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ kind:
+ description:
+ - The kind attribute of the object.
+ required: True
+ default: None
+ choices:
+ - bc
+ - buildconfig
+ - configmaps
+ - dc
+ - deploymentconfig
+ - imagestream
+ - imagestreamtag
+ - is
+ - istag
+ - namespace
+ - project
+ - projects
+ - node
+ - ns
+ - persistentvolume
+ - pv
+ - rc
+ - replicationcontroller
+ - routes
+ - scc
+ - secret
+ - securitycontextconstraints
+ - service
+ - svc
+ aliases: []
+ file_name:
+ description:
+ - The file name in which to edit
+ required: false
+ default: None
+ aliases: []
+ file_format:
+ description:
+ - The format of the file being edited.
+ required: false
+ default: yaml
+ aliases: []
+ content:
+ description:
+ - Content of the file
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to force the operation
+ required: false
+ default: None
+ aliases: []
+ separator:
+ description:
+ - The separator format for the edit.
+ required: false
+ default: '.'
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_edit:
+ kind: rc
+ name: hawkular-cassandra-rc
+ namespace: openshift-infra
+ content:
+ spec.template.spec.containers[0].resources.limits.memory: 512
+ spec.template.spec.containers[0].resources.requests.memory: 256
+'''
diff --git a/roles/lib_openshift/src/doc/env b/roles/lib_openshift/src/doc/env
new file mode 100644
index 000000000..36edcd211
--- /dev/null
+++ b/roles/lib_openshift/src/doc/env
@@ -0,0 +1,83 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_env
+short_description: Modify, and idempotently manage openshift environment variables on pods, deploymentconfigs, and replication controllers.
+description:
+ - Modify openshift environment variables programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a list of environment variables
+ - absent - will remove the environment varibale from the object
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ kind:
+ description:
+ - The kind attribute of the object.
+ required: False
+ default: dc
+ choices:
+ - rc
+ - dc
+ - pods
+ aliases: []
+ env_vars:
+ description:
+ - The environment variables to insert. The format is a dict of value pairs.
+ - e.g. {key1: value1, key2: value2})
+ required: False
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: query a list of env vars on dc
+ oc_env:
+ kind: dc
+ name: myawesomedc
+ namespace: phpapp
+
+- name: Set the following variables.
+ oc_env:
+ kind: dc
+ name: myawesomedc
+ namespace: phpapp
+ env_vars:
+ SUPER_TURBO_MODE: 'true'
+ ENABLE_PORTS: 'false'
+ SERVICE_PORT: 9999
+'''
diff --git a/roles/lib_openshift/src/doc/generated b/roles/lib_openshift/src/doc/generated
new file mode 100644
index 000000000..b55d18cff
--- /dev/null
+++ b/roles/lib_openshift/src/doc/generated
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+# flake8: noqa: T001
+# ___ ___ _ _ ___ ___ _ _____ ___ ___
+# / __| __| \| | __| _ \ /_\_ _| __| \
+# | (_ | _|| .` | _|| / / _ \| | | _|| |) |
+# \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
+# | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
+# | |) | (_) | | .` | (_) || | | _|| |) | | | |
+# |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
diff --git a/roles/lib_openshift/src/doc/group b/roles/lib_openshift/src/doc/group
new file mode 100644
index 000000000..c5ba6ebd9
--- /dev/null
+++ b/roles/lib_openshift/src/doc/group
@@ -0,0 +1,56 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_group
+short_description: Modify, and idempotently manage openshift groups.
+description:
+ - Modify openshift groups programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a group
+ - absent - will remove the group
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create group
+ oc_group:
+ state: present
+ name: acme_org
+ register: group_out
+'''
diff --git a/roles/lib_openshift/src/doc/image b/roles/lib_openshift/src/doc/image
new file mode 100644
index 000000000..18cf4e168
--- /dev/null
+++ b/roles/lib_openshift/src/doc/image
@@ -0,0 +1,75 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_image
+short_description: Create, modify, and idempotently manage openshift labels.
+description:
+ - Modify openshift labels programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create. Does _not_ support update.
+ - 'list' will read the labels
+ default: present
+ choices: ["present", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace where this object lives
+ required: false
+ default: default
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ registry_url:
+ description:
+ - The url for the registry so that openshift can pull the image
+ required: false
+ default: None
+ aliases: []
+ image_name:
+ description:
+ - The name of the image being imported
+ required: false
+ default: False
+ aliases: []
+ image_tag:
+ description:
+ - The tag of the image being imported
+ required: false
+ default: None
+ aliases: []
+author:
+- "Ivan Horvath<ihorvath@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Get an imagestream
+ oc_image:
+ name: php55
+ state: list
+ register: imageout
+
+- name: create an imagestream
+ oc_image:
+ state: present
+ image_name: php55
+ image_tag: int
+ registry_url: registry.example.com
+ namespace: default
+ register: imageout
+'''
diff --git a/roles/lib_openshift/src/doc/label b/roles/lib_openshift/src/doc/label
new file mode 100644
index 000000000..fb3ed2503
--- /dev/null
+++ b/roles/lib_openshift/src/doc/label
@@ -0,0 +1,92 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_label
+short_description: Create, modify, and idempotently manage openshift labels.
+description:
+ - Modify openshift labels programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update and object to the desired state
+ - 'absent' will ensure certain labels are removed
+ - 'list' will read the labels
+ - 'add' will insert labels to the already existing labels
+ default: present
+ choices: ["present", "absent", "list", "add"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ kind:
+ description:
+ - The kind of object that can be managed.
+ default: node
+ choices:
+ - node
+ - pod
+ - namespace
+ aliases: []
+ labels:
+ description:
+ - A list of labels for the resource.
+ - Each list consists of a key and a value.
+ - eg, {'key': 'foo', 'value': 'bar'}
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - The selector to apply to the resource query
+ required: false
+ default: None
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Add a single label to a node's existing labels
+ oc_label:
+ name: ip-172-31-5-23.ec2.internal
+ state: add
+ kind: node
+ labels:
+ - key: logging-infra-fluentd
+ value: 'true'
+
+- name: remove a label from a node
+ oc_label:
+ name: ip-172-31-5-23.ec2.internal
+ state: absent
+ kind: node
+ labels:
+ - key: color
+ value: blue
+
+- name: Ensure node has these exact labels
+ oc_label:
+ name: ip-172-31-5-23.ec2.internal
+ state: present
+ kind: node
+ labels:
+ - key: color
+ value: green
+ - key: type
+ value: master
+ - key: environment
+ value: production
+'''
diff --git a/roles/lib_openshift/src/doc/license b/roles/lib_openshift/src/doc/license
new file mode 100644
index 000000000..717bb7f17
--- /dev/null
+++ b/roles/lib_openshift/src/doc/license
@@ -0,0 +1,16 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/roles/lib_openshift/src/doc/manage_node b/roles/lib_openshift/src/doc/manage_node
new file mode 100644
index 000000000..b651ea4e7
--- /dev/null
+++ b/roles/lib_openshift/src/doc/manage_node
@@ -0,0 +1,88 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_manage_node
+short_description: Module to manage openshift nodes
+description:
+ - Manage openshift nodes programmatically.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ node:
+ description:
+ - A list of the nodes being managed
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - The selector when filtering on node labels
+ required: false
+ default: None
+ aliases: []
+ pod_selector:
+ description:
+ - A selector when filtering on pod labels.
+ required: false
+ default: None
+ aliases: []
+ evacuate:
+ description:
+ - Remove all pods from a node.
+ required: false
+ default: False
+ aliases: []
+ schedulable:
+ description:
+ - whether or not openshift can schedule pods on this node
+ required: False
+ default: None
+ aliases: []
+ dry_run:
+ description:
+ - This shows the pods that would be migrated if evacuate were called
+ required: False
+ default: False
+ aliases: []
+ grace_period:
+ description:
+ - Grace period (seconds) for pods being deleted.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to attempt to force this action in openshift
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oadm manage-node --schedulable=true --selector=ops_node=new
+ oc_adm_manage_node:
+ selector: ops_node=new
+ schedulable: True
+ register: schedout
+
+- name: oadm manage-node my-k8s-node-5 --evacuate
+ oc_adm_manage_node:
+ node: my-k8s-node-5
+ evacuate: True
+ force: True
+'''
diff --git a/roles/lib_openshift/src/doc/obj b/roles/lib_openshift/src/doc/obj
new file mode 100644
index 000000000..c6504ed01
--- /dev/null
+++ b/roles/lib_openshift/src/doc/obj
@@ -0,0 +1,95 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_obj
+short_description: Generic interface to openshift objects
+description:
+ - Manage openshift objects programmatically.
+options:
+ state:
+ description:
+ - Currently present is only supported state.
+ required: true
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ all_namespaces:
+ description:
+ - Search in all namespaces for the object.
+ required: false
+ default: false
+ aliases: []
+ kind:
+ description:
+ - The kind attribute of the object. e.g. dc, bc, svc, route. May be a comma-separated list, e.g. "dc,po,svc".
+ required: True
+ default: None
+ aliases: []
+ files:
+ description:
+ - A list of files provided for object
+ required: false
+ default: None
+ aliases: []
+ delete_after:
+ description:
+ - Whether or not to delete the files after processing them.
+ required: false
+ default: false
+ aliases: []
+ content:
+ description:
+ - Content of the object being managed.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to force the operation
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - Selector that gets added to the query.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_obj:
+ kind: dc
+ name: router
+ namespace: default
+register: router_output
+'''
diff --git a/roles/lib_openshift/src/doc/objectvalidator b/roles/lib_openshift/src/doc/objectvalidator
new file mode 100644
index 000000000..98861e261
--- /dev/null
+++ b/roles/lib_openshift/src/doc/objectvalidator
@@ -0,0 +1,27 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_objectvalidator
+short_description: Validate OpenShift objects
+description:
+ - Validate OpenShift objects
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+author:
+- "Mo Khan <monis@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_objectvalidator:
+- name: run oc_objectvalidator
+ oc_objectvalidator:
+ register: oc_objectvalidator
+'''
diff --git a/roles/lib_openshift/src/doc/policy_group b/roles/lib_openshift/src/doc/policy_group
new file mode 100644
index 000000000..343413269
--- /dev/null
+++ b/roles/lib_openshift/src/doc/policy_group
@@ -0,0 +1,74 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_policy_group
+short_description: Module to manage openshift policy for groups
+description:
+ - Manage openshift policy for groups.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace scope
+ required: false
+ default: None
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ group:
+ description:
+ - The name of the group
+ required: true
+ default: None
+ aliases: []
+ resource_kind:
+ description:
+ - The kind of policy to affect
+ required: true
+ default: None
+ choices: ["role", "cluster-role", "scc"]
+ aliases: []
+ resource_name:
+ description:
+ - The name of the policy
+ required: true
+ default: None
+ aliases: []
+ state:
+ description:
+ - Desired state of the policy
+ required: true
+ default: present
+ choices: ["present", "absent"]
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oc adm policy remove-scc-from-group an-scc agroup
+ oc_adm_policy_group:
+ group: agroup
+ resource_kind: scc
+ resource_name: an-scc
+ state: absent
+
+- name: oc adm policy add-cluster-role-to-group system:build-strategy-docker agroup
+ oc_adm_policy_group:
+ group: agroup
+ resource_kind: cluster-role
+ resource_name: system:build-strategy-docker
+ state: present
+'''
diff --git a/roles/lib_openshift/src/doc/policy_user b/roles/lib_openshift/src/doc/policy_user
new file mode 100644
index 000000000..351c9af65
--- /dev/null
+++ b/roles/lib_openshift/src/doc/policy_user
@@ -0,0 +1,74 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_policy_user
+short_description: Module to manage openshift policy for users
+description:
+ - Manage openshift policy for users.
+options:
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ namespace:
+ description:
+ - The namespace scope
+ required: false
+ default: None
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ user:
+ description:
+ - The name of the user
+ required: true
+ default: None
+ aliases: []
+ resource_kind:
+ description:
+ - The kind of policy to affect
+ required: true
+ default: None
+ choices: ["role", "cluster-role", "scc"]
+ aliases: []
+ resource_name:
+ description:
+ - The name of the policy
+ required: true
+ default: None
+ aliases: []
+ state:
+ description:
+ - Desired state of the policy
+ required: true
+ default: present
+ choices: ["present", "absent"]
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: oc adm policy remove-scc-from-user an-scc ausername
+ oc_adm_policy_user:
+ user: ausername
+ resource_kind: scc
+ resource_name: an-scc
+ state: absent
+
+- name: oc adm policy add-cluster-role-to-user system:build-strategy-docker ausername
+ oc_adm_policy_user:
+ user: ausername
+ resource_kind: cluster-role
+ resource_name: system:build-strategy-docker
+ state: present
+'''
diff --git a/roles/lib_openshift/src/doc/process b/roles/lib_openshift/src/doc/process
new file mode 100644
index 000000000..86a854c07
--- /dev/null
+++ b/roles/lib_openshift/src/doc/process
@@ -0,0 +1,84 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_process
+short_description: Module to process openshift templates
+description:
+ - Process openshift templates programmatically.
+options:
+ state:
+ description:
+ - State has a few different meanings when it comes to process.
+ - state: present - This state runs an `oc process <template>`. When used in
+ - conjunction with 'create: True' the process will be piped to | oc create -f
+ - state: absent - will remove a template
+ - state: list - will perform an `oc get template <template_name>`
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ template_name:
+ description:
+ - Name of the openshift template that is being processed.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the template lives.
+ required: false
+ default: default
+ aliases: []
+ content:
+ description:
+ - Template content that will be processed.
+ required: false
+ default: None
+ aliases: []
+ params:
+ description:
+ - A list of parameters that will be inserted into the template.
+ required: false
+ default: None
+ aliases: []
+ create:
+ description:
+ - Whether or not to create the template after being processed. e.g. oc process | oc create -f -
+ required: False
+ default: False
+ aliases: []
+ reconcile:
+ description:
+ - Whether or not to attempt to determine if there are updates or changes in the incoming template.
+ default: true
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: process the cloud volume provisioner template with variables
+ oc_process:
+ namespace: openshift-infra
+ template_name: online-volume-provisioner
+ create: True
+ params:
+ PLAT: rhel7
+ register: processout
+ run_once: true
+- debug: var=processout
+'''
diff --git a/roles/lib_openshift/src/doc/project b/roles/lib_openshift/src/doc/project
new file mode 100644
index 000000000..92efe4320
--- /dev/null
+++ b/roles/lib_openshift/src/doc/project
@@ -0,0 +1,81 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_project
+short_description: Module to manage openshift projects
+description:
+ - Manage openshift projects programmatically.
+options:
+ state:
+ description:
+ - If present, the project will be created if it doesn't exist or update if different. If absent, the project will be removed if present. If list, information about the project will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ display_name:
+ description:
+ - The display name attribute for a project
+ required: false
+ default: None
+ aliases: []
+ description:
+ description:
+ - The description attribute for a project
+ required: false
+ default: None
+ aliases: []
+ admin:
+ description:
+ - The project admin username
+ required: false
+ default: false
+ aliases: []
+ admin_role:
+ description:
+ - The project admin username
+ required: false
+ default: 'admin'
+ aliases: []
+ node_selector:
+ description:
+ - The node selector for this project.
+ - This allows certain pods in this project to run on certain nodes.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create secret
+ oc_project:
+ state: present
+ name: openshift-ops
+ display_name: operations team project
+ node_selector:
+ - top=secret
+ - noncustomer=True
+'''
diff --git a/roles/lib_openshift/src/doc/pvc b/roles/lib_openshift/src/doc/pvc
new file mode 100644
index 000000000..268ad0b94
--- /dev/null
+++ b/roles/lib_openshift/src/doc/pvc
@@ -0,0 +1,88 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_pvc
+short_description: Modify, and idempotently manage openshift persistent volume claims
+description:
+ - Modify openshift persistent volume claims programmatically.
+options:
+ state:
+ description:
+ - Supported states, present, absent, list
+ - present - will ensure object is created or updated to the value specified
+ - list - will return a pvc
+ - absent - will remove a pvc
+ required: False
+ default: present
+ choices: ["present", 'absent', 'list']
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ volume_capacity:
+ description:
+ - The requested volume capacity
+ required: False
+ default: 1G
+ aliases: []
+ access_modes:
+ description:
+ - The access modes allowed for the pvc
+ - Expects a list
+ required: False
+ default: ReadWriteOnce
+ choices:
+ - ReadWriteOnce
+ - ReadOnlyMany
+ - ReadWriteMany
+ aliases: []
+ storage_class_name:
+ description:
+ - The storage class name for the PVC
+ required: false
+ default: None
+ aliases: []
+ selector:
+ description:
+ - A hash of key/values for the matchLabels
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create a pvc
+ oc_pvc:
+ namespace: awesomeapp
+ name: dbstorage
+ access_modes:
+ - ReadWriteOnce
+ volume_capacity: 5G
+ register: pvcout
+'''
diff --git a/roles/lib_openshift/src/doc/registry b/roles/lib_openshift/src/doc/registry
new file mode 100644
index 000000000..ebc714b7a
--- /dev/null
+++ b/roles/lib_openshift/src/doc/registry
@@ -0,0 +1,192 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_registry
+short_description: Module to manage openshift registry
+description:
+ - Manage openshift registry programmatically.
+options:
+ state:
+ description:
+ - The desired action when managing openshift registry
+ - present - update or create the registry
+ - absent - tear down the registry service and deploymentconfig
+ - list - returns the current representiation of a registry
+ required: false
+ default: False
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - The name of the registry
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The selector when filtering on node labels
+ required: false
+ default: None
+ aliases: []
+ images:
+ description:
+ - The image to base this registry on - ${component} will be replaced with --type
+ required: 'openshift3/ose-${component}:${version}'
+ default: None
+ aliases: []
+ latest_images:
+ description:
+ - If true, attempt to use the latest image for the registry instead of the latest release.
+ required: false
+ default: False
+ aliases: []
+ labels:
+ description:
+ - A set of labels to uniquely identify the registry and its components.
+ required: false
+ default: None
+ aliases: []
+ enforce_quota:
+ description:
+ - If set, the registry will refuse to write blobs if they exceed quota limits
+ required: False
+ default: False
+ aliases: []
+ mount_host:
+ description:
+ - If set, the registry volume will be created as a host-mount at this path.
+ required: False
+ default: False
+ aliases: []
+ ports:
+ description:
+ - A comma delimited list of ports or port pairs to expose on the registry pod. The default is set for 5000.
+ required: False
+ default: [5000]
+ aliases: []
+ replicas:
+ description:
+ - The replication factor of the registry; commonly 2 when high availability is desired.
+ required: False
+ default: 1
+ aliases: []
+ selector:
+ description:
+ - Selector used to filter nodes on deployment. Used to run registries on a specific set of nodes.
+ required: False
+ default: None
+ aliases: []
+ service_account:
+ description:
+ - Name of the service account to use to run the registry pod.
+ required: False
+ default: 'registry'
+ aliases: []
+ tls_certificate:
+ description:
+ - An optional path to a PEM encoded certificate (which may contain the private key) for serving over TLS
+ required: false
+ default: None
+ aliases: []
+ tls_key:
+ description:
+ - An optional path to a PEM encoded private key for serving over TLS
+ required: false
+ default: None
+ aliases: []
+ volume_mounts:
+ description:
+ - The volume mounts for the registry.
+ required: false
+ default: None
+ aliases: []
+ daemonset:
+ description:
+ - Use a daemonset instead of a deployment config.
+ required: false
+ default: False
+ aliases: []
+ edits:
+ description:
+ - A list of modifications to make on the deploymentconfig
+ required: false
+ default: None
+ aliases: []
+ env_vars:
+ description:
+ - A dictionary of modifications to make on the deploymentconfig. e.g. FOO: BAR
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Force a registry update.
+ required: false
+ default: False
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create a secure registry
+ oc_adm_registry:
+ name: docker-registry
+ service_account: registry
+ replicas: 2
+ namespace: default
+ selector: type=infra
+ images: "registry.ops.openshift.com/openshift3/ose-${component}:${version}"
+ env_vars:
+ REGISTRY_CONFIGURATION_PATH: /etc/registryconfig/config.yml
+ REGISTRY_HTTP_TLS_CERTIFICATE: /etc/secrets/registry.crt
+ REGISTRY_HTTP_TLS_KEY: /etc/secrets/registry.key
+ REGISTRY_HTTP_SECRET: supersecret
+ volume_mounts:
+ - path: /etc/secrets
+ name: dockercerts
+ type: secret
+ secret_name: registry-secret
+ - path: /etc/registryconfig
+ name: dockersecrets
+ type: secret
+ secret_name: docker-registry-config
+ edits:
+ - key: spec.template.spec.containers[0].livenessProbe.httpGet.scheme
+ value: HTTPS
+ action: put
+ - key: spec.template.spec.containers[0].readinessProbe.httpGet.scheme
+ value: HTTPS
+ action: put
+ - key: spec.strategy.rollingParams
+ value:
+ intervalSeconds: 1
+ maxSurge: 50%
+ maxUnavailable: 50%
+ timeoutSeconds: 600
+ updatePeriodSeconds: 1
+ action: put
+ - key: spec.template.spec.containers[0].resources.limits.memory
+ value: 2G
+ action: update
+ - key: spec.template.spec.containers[0].resources.requests.memory
+ value: 1G
+ action: update
+
+ register: registryout
+
+'''
diff --git a/roles/lib_openshift/src/doc/route b/roles/lib_openshift/src/doc/route
new file mode 100644
index 000000000..f0d38ab5f
--- /dev/null
+++ b/roles/lib_openshift/src/doc/route
@@ -0,0 +1,132 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_route
+short_description: Create, modify, and idempotently manage openshift routes.
+description:
+ - Manage openshift route objects programmatically.
+options:
+ state:
+ description:
+ - State represents whether to create, modify, delete, or list
+ required: true
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: str
+ aliases: []
+ labels:
+ description:
+ - The labels to apply on the route
+ required: false
+ default: None
+ aliases: []
+ tls_termination:
+ description:
+ - The options for termination. e.g. reencrypt
+ required: false
+ default: None
+ aliases: []
+ dest_cacert_path:
+ description:
+ - The path to the dest_cacert
+ required: false
+ default: None
+ aliases: []
+ cacert_path:
+ description:
+ - The path to the cacert
+ required: false
+ default: None
+ aliases: []
+ cert_path:
+ description:
+ - The path to the cert
+ required: false
+ default: None
+ aliases: []
+ key_path:
+ description:
+ - The path to the key
+ required: false
+ default: None
+ aliases: []
+ dest_cacert_content:
+ description:
+ - The dest_cacert content
+ required: false
+ default: None
+ aliases: []
+ cacert_content:
+ description:
+ - The cacert content
+ required: false
+ default: None
+ aliases: []
+ cert_content:
+ description:
+ - The cert content
+ required: false
+ default: None
+ aliases: []
+ service_name:
+ description:
+ - The name of the service that this route points to.
+ required: false
+ default: None
+ aliases: []
+ host:
+ description:
+ - The host that the route will use. e.g. myapp.x.y.z
+ required: false
+ default: None
+ aliases: []
+ port:
+ description:
+ - The Name of the service port or number of the container port the route will route traffic to
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Configure certificates for reencrypt route
+ oc_route:
+ name: myapproute
+ namespace: awesomeapp
+ cert_path: "/etc/origin/master/named_certificates/myapp_cert
+ key_path: "/etc/origin/master/named_certificates/myapp_key
+ cacert_path: "/etc/origin/master/named_certificates/myapp_cacert
+ dest_cacert_content: "{{ dest_cacert_content }}"
+ service_name: myapp_php
+ host: myapp.awesomeapp.openshift.com
+ tls_termination: reencrypt
+ run_once: true
+'''
diff --git a/roles/lib_openshift/src/doc/router b/roles/lib_openshift/src/doc/router
new file mode 100644
index 000000000..ed46e03a0
--- /dev/null
+++ b/roles/lib_openshift/src/doc/router
@@ -0,0 +1,217 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_adm_router
+short_description: Module to manage openshift router
+description:
+ - Manage openshift router programmatically.
+options:
+ state:
+ description:
+ - Whether to create or delete the router
+ - present - create the router
+ - absent - remove the router
+ - list - return the current representation of a router
+ required: false
+ default: present
+ choices:
+ - present
+ - absent
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - The name of the router
+ required: false
+ default: router
+ aliases: []
+ namespace:
+ description:
+ - The namespace where to manage the router.
+ required: false
+ default: default
+ aliases: []
+ images:
+ description:
+ - The image to base this router on - ${component} will be replaced with --type
+ required: 'openshift3/ose-${component}:${version}'
+ default: None
+ aliases: []
+ latest_images:
+ description:
+ - If true, attempt to use the latest image for the registry instead of the latest release.
+ required: false
+ default: False
+ aliases: []
+ labels:
+ description:
+ - A set of labels to uniquely identify the registry and its components.
+ required: false
+ default: None
+ aliases: []
+ ports:
+ description:
+ - A list of strings in the 'port:port' format
+ required: False
+ default:
+ - 80:80
+ - 443:443
+ aliases: []
+ replicas:
+ description:
+ - The replication factor of the registry; commonly 2 when high availability is desired.
+ required: False
+ default: 1
+ aliases: []
+ selector:
+ description:
+ - Selector used to filter nodes on deployment. Used to run routers on a specific set of nodes.
+ required: False
+ default: None
+ aliases: []
+ service_account:
+ description:
+ - Name of the service account to use to run the router pod.
+ required: False
+ default: router
+ aliases: []
+ router_type:
+ description:
+ - The router image to use - if you specify --images this flag may be ignored.
+ required: false
+ default: haproxy-router
+ aliases: []
+ external_host:
+ description:
+ - If the underlying router implementation connects with an external host, this is the external host's hostname.
+ required: false
+ default: None
+ aliases: []
+ external_host_vserver:
+ description:
+ - If the underlying router implementation uses virtual servers, this is the name of the virtual server for HTTP connections.
+ required: false
+ default: None
+ aliases: []
+ external_host_insecure:
+ description:
+ - If the underlying router implementation connects with an external host
+ - over a secure connection, this causes the router to skip strict certificate verification with the external host.
+ required: false
+ default: False
+ aliases: []
+ external_host_partition_path:
+ description:
+ - If the underlying router implementation uses partitions for control boundaries, this is the path to use for that partition.
+ required: false
+ default: None
+ aliases: []
+ external_host_username:
+ description:
+ - If the underlying router implementation connects with an external host, this is the username for authenticating with the external host.
+ required: false
+ default: None
+ aliases: []
+ external_host_password:
+ description:
+ - If the underlying router implementation connects with an external host, this is the password for authenticating with the external host.
+ required: false
+ default: None
+ aliases: []
+ external_host_private_key:
+ description:
+ - If the underlying router implementation requires an SSH private key, this is the path to the private key file.
+ required: false
+ default: None
+ aliases: []
+ expose_metrics:
+ description:
+ - This is a hint to run an extra container in the pod to expose metrics - the image
+ - will either be set depending on the router implementation or provided with --metrics-image.
+ required: false
+ default: False
+ aliases: []
+ metrics_image:
+ description:
+ - If expose_metrics is specified this is the image to use to run a sidecar container
+ - in the pod exposing metrics. If not set and --expose-metrics is true the image will
+ - depend on router implementation.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment:
+- There are some exceptions to note when doing the idempotency in this module.
+- The strategy is to use the oc adm router command to generate a default
+- configuration when creating or updating a router. Often times there
+- differences from the generated template and what is in memory in openshift.
+- We make exceptions to not check these specific values when comparing objects.
+- Here are a list of exceptions:
+- - DeploymentConfig:
+ - dnsPolicy
+ - terminationGracePeriodSeconds
+ - restartPolicy
+ - timeoutSeconds
+ - livenessProbe
+ - readinessProbe
+ - terminationMessagePath
+ - hostPort
+ - defaultMode
+ - Service:
+ - portalIP
+ - clusterIP
+ - sessionAffinity
+ - type
+ - ServiceAccount:
+ - secrets
+ - imagePullSecrets
+'''
+
+EXAMPLES = '''
+- name: create routers
+ oc_adm_router:
+ name: router
+ service_account: router
+ replicas: 2
+ namespace: default
+ selector: type=infra
+ cert_file: /etc/origin/master/named_certificates/router.crt
+ key_file: /etc/origin/master/named_certificates/router.key
+ cacert_file: /etc/origin/master/named_certificates/router.ca
+ edits:
+ - key: spec.strategy.rollingParams
+ value:
+ intervalSeconds: 1
+ maxSurge: 50%
+ maxUnavailable: 50%
+ timeoutSeconds: 600
+ updatePeriodSeconds: 1
+ action: put
+ - key: spec.template.spec.containers[0].resources.limits.memory
+ value: 2G
+ action: put
+ - key: spec.template.spec.containers[0].resources.requests.memory
+ value: 1G
+ action: put
+ - key: spec.template.spec.containers[0].env
+ value:
+ name: EXTENDED_VALIDATION
+ value: 'false'
+ action: update
+ register: router_out
+ run_once: True
+'''
diff --git a/roles/lib_openshift/src/doc/scale b/roles/lib_openshift/src/doc/scale
new file mode 100644
index 000000000..e98730911
--- /dev/null
+++ b/roles/lib_openshift/src/doc/scale
@@ -0,0 +1,70 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_scale
+short_description: Manage openshift services through the scale parameters
+description:
+ - Manage openshift services through scaling them.
+options:
+ state:
+ description:
+ - State represents whether to scale or list the current replicas
+ required: true
+ default: present
+ choices: ["present", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ kind:
+ description:
+ - The kind of object to scale.
+ required: false
+ default: None
+ choices:
+ - rc
+ - dc
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: scale down a rc to 0
+ oc_scale:
+ name: my-replication-controller
+ kind: rc
+ namespace: openshift-infra
+ replicas: 0
+
+- name: scale up a deploymentconfig to 2
+ oc_scale:
+ name: php
+ kind: dc
+ namespace: my-php-app
+ replicas: 2
+'''
diff --git a/roles/lib_openshift/src/doc/secret b/roles/lib_openshift/src/doc/secret
new file mode 100644
index 000000000..a27f90f38
--- /dev/null
+++ b/roles/lib_openshift/src/doc/secret
@@ -0,0 +1,128 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_secret
+short_description: Module to manage openshift secrets
+description:
+ - Manage openshift secrets programmatically.
+options:
+ state:
+ description:
+ - If present, the secret will be created if it doesn't exist or updated if different. If absent, the secret will be removed if present. If list, information about the secret will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ annotations:
+ description:
+ - Annotations to apply to the object
+ required: false
+ default: None
+ aliases: []
+ files:
+ description:
+ - A list of files provided for secrets
+ required: false
+ default: None
+ aliases: []
+ delete_after:
+ description:
+ - Whether or not to delete the files after processing them.
+ required: false
+ default: false
+ aliases: []
+ contents:
+ description:
+ - Content of the secrets
+ required: false
+ default: None
+ aliases: []
+ type:
+ description:
+ - The secret type.
+ required: false
+ default: None
+ aliases: []
+ force:
+ description:
+ - Whether or not to force the operation
+ required: false
+ default: false
+ aliases: []
+ decode:
+ description:
+ - base64 decode the object
+ required: false
+ default: false
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create secret
+ oc_secret:
+ state: present
+ namespace: openshift-infra
+ name: metrics-deployer
+ files:
+ - name: nothing
+ path: /dev/null
+ register: secretout
+ run_once: true
+
+- name: get ca from hawkular
+ oc_secret:
+ state: list
+ namespace: openshift-infra
+ name: hawkular-metrics-certificate
+ decode: True
+ register: hawkout
+ run_once: true
+
+- name: Create secrets
+ oc_secret:
+ namespace: mynamespace
+ name: mysecrets
+ contents:
+ - path: data.yml
+ data: "{{ data_content }}"
+ - path: auth-keys
+ data: "{{ auth_keys_content }}"
+ - path: configdata.yml
+ data: "{{ configdata_content }}"
+ - path: cert.crt
+ data: "{{ cert_content }}"
+ - path: key.pem
+ data: "{{ osso_site_key_content }}"
+ - path: ca.cert.pem
+ data: "{{ ca_cert_content }}"
+ register: secretout
+'''
diff --git a/roles/lib_openshift/src/doc/service b/roles/lib_openshift/src/doc/service
new file mode 100644
index 000000000..ba9aa0b38
--- /dev/null
+++ b/roles/lib_openshift/src/doc/service
@@ -0,0 +1,129 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_service
+short_description: Create, modify, and idempotently manage openshift services.
+description:
+ - Manage openshift service objects programmatically.
+options:
+ state:
+ description:
+ - State represents whether to create, modify, delete, or list
+ required: False
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The namespace where the object lives.
+ required: false
+ default: default
+ aliases: []
+ selector:
+ description:
+ - The selector to apply when filtering for services.
+ required: false
+ default: None
+ aliases: []
+ labels:
+ description:
+ - The labels to apply on the service.
+ required: false
+ default: None
+ aliases: []
+ clusterip:
+ description:
+ - The cluster ip address to use with this service.
+ required: false
+ default: None
+ aliases: []
+ portalip:
+ description:
+ - The portal ip(virtual ip) address to use with this service.
+ - "https://docs.openshift.com/enterprise/3.0/architecture/core_concepts/pods_and_services.html#services"
+ required: false
+ default: None
+ aliases: []
+ ports:
+ description:
+ - A list of the ports that are used for this service. This includes name, port, protocol, and targetPort.
+ - See examples.
+ required: false
+ default: None
+ aliases: []
+ session_affinity:
+ description:
+ - The type of session affinity to use.
+ required: false
+ default: 'None'
+ aliases: []
+ service_type:
+ description:
+ - The type of service desired. Each option tells the service to behave accordingly.
+ - https://kubernetes.io/docs/user-guide/services/
+ required: false
+ default: ClusterIP
+ choices:
+ - ClusterIP
+ - NodePort
+ - LoadBalancer
+ - ExternalName
+ aliases: []
+ externalips:
+ description:
+ - A list of the external IPs that are exposed for this service.
+ - https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: get docker-registry service
+ run_once: true
+ oc_service:
+ namespace: default
+ name: docker-registry
+ state: list
+ register: registry_service_out
+
+- name: create the docker-registry service
+ oc_service:
+ namespace: default
+ name: docker-registry
+ ports:
+ - name: 5000-tcp
+ port: 5000
+ protocol: TCP
+ targetPort: 5000
+ selector:
+ docker-registry: default
+ session_affinity: ClientIP
+ service_type: ClusterIP
+ register: svc_out
+ notify:
+ - restart openshift master services
+'''
diff --git a/roles/lib_openshift/src/doc/serviceaccount b/roles/lib_openshift/src/doc/serviceaccount
new file mode 100644
index 000000000..b2eafab51
--- /dev/null
+++ b/roles/lib_openshift/src/doc/serviceaccount
@@ -0,0 +1,68 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_serviceaccount
+short_description: Module to manage openshift service accounts
+description:
+ - Manage openshift service accounts programmatically.
+options:
+ state:
+ description:
+ - If present, the service account will be created if it doesn't exist or updated if different. If absent, the service account will be removed if present. If list, information about the service account will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: false
+ aliases: []
+ name:
+ description:
+ - Name of the service account.
+ required: true
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - Namespace of the service account.
+ required: true
+ default: default
+ aliases: []
+ secrets:
+ description:
+ - A list of secrets that are associated with the service account.
+ required: false
+ default: None
+ aliases: []
+ image_pull_secrets:
+ description:
+ - A list of the image pull secrets that are associated with the service account.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: create registry serviceaccount
+ oc_serviceaccount:
+ name: registry
+ namespace: default
+ secrets:
+ - docker-registry-config
+ - registry-secret
+ register: sa_out
+'''
diff --git a/roles/lib_openshift/src/doc/serviceaccount_secret b/roles/lib_openshift/src/doc/serviceaccount_secret
new file mode 100644
index 000000000..ab90a3f7c
--- /dev/null
+++ b/roles/lib_openshift/src/doc/serviceaccount_secret
@@ -0,0 +1,68 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_serviceaccount_secret
+short_description: Module to manage openshift service account secrets
+description:
+ - Manage openshift service account secrets programmatically.
+options:
+ state:
+ description:
+ - If present, the service account will be linked with the secret if it is not already. If absent, the service account will be unlinked from the secret if it is already linked. If list, information about the service account secrets will be gathered and returned as part of the Ansible call results.
+ required: false
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: false
+ aliases: []
+ service_account:
+ description:
+ - Name of the service account.
+ required: true
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - Namespace of the service account and secret.
+ required: true
+ default: None
+ aliases: []
+ secret:
+ description:
+ - The secret that should be linked to the service account.
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+ - name: get secrets of a service account
+ oc_serviceaccount_secret:
+ state: list
+ service_account: builder
+ namespace: default
+ register: sasecretout
+
+
+ - name: Link a service account to a specific secret
+ oc_serviceaccount_secret:
+ service_account: builder
+ secret: mynewsecret
+ namespace: default
+ register: sasecretout
+'''
diff --git a/roles/lib_openshift/src/doc/storageclass b/roles/lib_openshift/src/doc/storageclass
new file mode 100644
index 000000000..5a7320d55
--- /dev/null
+++ b/roles/lib_openshift/src/doc/storageclass
@@ -0,0 +1,86 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_storageclass
+short_description: Create, modify, and idempotently manage openshift storageclasses.
+description:
+ - Manage openshift storageclass objects programmatically.
+options:
+ state:
+ description:
+ - State represents whether to create, modify, delete, or list
+ required: False
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ provisioner:
+ description:
+ - Any annotations to add to the storageclass
+ required: false
+ default: 'aws-ebs'
+ aliases: []
+ default_storage_class:
+ description:
+ - Whether or not this is the default storage class
+ required: false
+ default: False
+ aliases: []
+ parameters:
+ description:
+ - A dictionary with the parameters to configure the storageclass. This will be based on provisioner
+ required: false
+ default: None
+ aliases: []
+ api_version:
+ description:
+ - The api version.
+ required: false
+ default: v1
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: get storageclass
+ run_once: true
+ oc_storageclass:
+ name: gp2
+ state: list
+ register: registry_sc_out
+
+- name: create the storageclass
+ oc_storageclass:
+ run_once: true
+ name: gp2
+ parameters:
+ type: gp2
+ encrypted: 'true'
+ kmsKeyId: '<full kms key arn>'
+ provisioner: aws-ebs
+ default_storage_class: False
+ register: sc_out
+ notify:
+ - restart openshift master services
+'''
diff --git a/roles/lib_openshift/src/doc/user b/roles/lib_openshift/src/doc/user
new file mode 100644
index 000000000..65ee01eb7
--- /dev/null
+++ b/roles/lib_openshift/src/doc/user
@@ -0,0 +1,128 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_user
+short_description: Create, modify, and idempotently manage openshift users.
+description:
+ - Modify openshift users programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update a user to the desired state
+ - 'absent' will ensure user is removed
+ - 'list' will read and return a list of users
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ username:
+ description:
+ - Short username to query/modify.
+ required: false
+ default: None
+ aliases: []
+ full_name:
+ description:
+ - String with the full name/description of the user.
+ required: false
+ default: None
+ aliases: []
+ groups:
+ description:
+ - List of groups the user should be a member of. This does not add/update the legacy 'groups' field in the OpenShift user object, but makes user entries into the appropriate OpenShift group object for the given user.
+ required: false
+ default: []
+ aliases: []
+author:
+- "Joel Diaz <jdiaz@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: Ensure user exists
+ oc_user:
+ state: present
+ username: johndoe
+ full_name "John Doe"
+ groups:
+ - dedicated-admins
+ register: user_johndoe
+
+user_johndoe variable will have contents like:
+ok: [ded-int-aws-master-61034] => {
+ "user_johndoe": {
+ "changed": true,
+ "results": {
+ "cmd": "oc -n default get users johndoe -o json",
+ "results": [
+ {
+ "apiVersion": "v1",
+ "fullName": "John DOe",
+ "groups": null,
+ "identities": null,
+ "kind": "User",
+ "metadata": {
+ "creationTimestamp": "2017-02-28T15:09:21Z",
+ "name": "johndoe",
+ "resourceVersion": "848781",
+ "selfLink": "/oapi/v1/users/johndoe",
+ "uid": "e23d3300-fdc7-11e6-9e3e-12822d6b7656"
+ }
+ }
+ ],
+ "returncode": 0
+ },
+ "state": "present"
+ }
+}
+'groups' is empty because this field is the OpenShift user object's 'group' field.
+
+- name: Ensure user does not exist
+ oc_user:
+ state: absent
+ username: johndoe
+
+- name: List user's info
+ oc_user:
+ state: list
+ username: johndoe
+ register: user_johndoe
+
+user_johndoe will have contents similar to:
+ok: [ded-int-aws-master-61034] => {
+ "user_johndoe": {
+ "changed": false,
+ "results": [
+ {
+ "apiVersion": "v1",
+ "fullName": "John Doe",
+ "groups": null,
+ "identities": null,
+ "kind": "User",
+ "metadata": {
+ "creationTimestamp": "2017-02-28T15:04:44Z",
+ "name": "johndoe",
+ "resourceVersion": "848280",
+ "selfLink": "/oapi/v1/users/johndoe",
+ "uid": "3d479ad2-fdc7-11e6-9e3e-12822d6b7656"
+ }
+ }
+ ],
+ "state": "list"
+ }
+}
+'''
diff --git a/roles/lib_openshift/src/doc/version b/roles/lib_openshift/src/doc/version
new file mode 100644
index 000000000..c0fdd53e7
--- /dev/null
+++ b/roles/lib_openshift/src/doc/version
@@ -0,0 +1,40 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_version
+short_description: Return the current openshift version
+description:
+ - Return the openshift installed version. `oc version`
+options:
+ state:
+ description:
+ - Currently list is only supported state.
+ required: true
+ default: list
+ choices: ["list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+oc_version:
+- name: get oc version
+ oc_version:
+ register: oc_version
+'''
diff --git a/roles/lib_openshift/src/doc/volume b/roles/lib_openshift/src/doc/volume
new file mode 100644
index 000000000..43ff78c9f
--- /dev/null
+++ b/roles/lib_openshift/src/doc/volume
@@ -0,0 +1,117 @@
+# flake8: noqa
+# pylint: skip-file
+
+DOCUMENTATION = '''
+---
+module: oc_volume
+short_description: Create, modify, and idempotently manage openshift volumes.
+description:
+ - Modify openshift volumes programmatically.
+options:
+ state:
+ description:
+ - State controls the action that will be taken with resource
+ - 'present' will create or update and object to the desired state
+ - 'absent' will ensure volumes are removed
+ - 'list' will read the volumes
+ default: present
+ choices: ["present", "absent", "list"]
+ aliases: []
+ kubeconfig:
+ description:
+ - The path for the kubeconfig file to use for authentication
+ required: false
+ default: /etc/origin/master/admin.kubeconfig
+ aliases: []
+ debug:
+ description:
+ - Turn on debug output.
+ required: false
+ default: False
+ aliases: []
+ name:
+ description:
+ - Name of the object that is being queried.
+ required: false
+ default: None
+ aliases: []
+ vol_name:
+ description:
+ - Name of the volume that is being queried.
+ required: false
+ default: None
+ aliases: []
+ namespace:
+ description:
+ - The name of the namespace where the object lives
+ required: false
+ default: default
+ aliases: []
+ kind:
+ description:
+ - The kind of object that can be managed.
+ default: dc
+ choices:
+ - dc
+ - rc
+ - pods
+ aliases: []
+ mount_type:
+ description:
+ - The type of volume to be used
+ required: false
+ default: None
+ choices:
+ - emptydir
+ - hostpath
+ - secret
+ - pvc
+ - configmap
+ aliases: []
+ mount_path:
+ description:
+ - The path to where the mount will be attached
+ required: false
+ default: None
+ aliases: []
+ secret_name:
+ description:
+ - The name of the secret. Used when mount_type is secret.
+ required: false
+ default: None
+ aliases: []
+ claim_size:
+ description:
+ - The size in GB of the pv claim. e.g. 100G
+ required: false
+ default: None
+ aliases: []
+ claim_name:
+ description:
+ - The name of the pv claim
+ required: false
+ default: None
+ aliases: []
+ configmap_name:
+ description:
+ - The name of the configmap
+ required: false
+ default: None
+ aliases: []
+author:
+- "Kenny Woodson <kwoodson@redhat.com>"
+extends_documentation_fragment: []
+'''
+
+EXAMPLES = '''
+- name: attach storage volumes to deploymentconfig
+ oc_volume:
+ namespace: logging
+ kind: dc
+ name: name_of_the_dc
+ mount_type: pvc
+ claim_name: loggingclaim
+ claim_size: 100G
+ vol_name: logging-storage
+ run_once: true
+'''
diff --git a/roles/lib_openshift/src/generate.py b/roles/lib_openshift/src/generate.py
new file mode 100755
index 000000000..2570f51dd
--- /dev/null
+++ b/roles/lib_openshift/src/generate.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+'''
+ Generate the openshift-ansible/roles/lib_openshift_cli/library/ modules.
+'''
+
+import argparse
+import os
+import re
+import yaml
+import six
+
+OPENSHIFT_ANSIBLE_PATH = os.path.dirname(os.path.realpath(__file__))
+OPENSHIFT_ANSIBLE_SOURCES_PATH = os.path.join(OPENSHIFT_ANSIBLE_PATH, 'sources.yml') # noqa: E501
+LIBRARY = os.path.join(OPENSHIFT_ANSIBLE_PATH, '..', 'library/')
+SKIP_COVERAGE_PATTERN = [re.compile('class Yedit.*$'),
+ re.compile('class Utils.*$')]
+PRAGMA_STRING = ' # pragma: no cover'
+
+
+class GenerateAnsibleException(Exception):
+ '''General Exception for generate function'''
+ pass
+
+
+def parse_args():
+ '''parse arguments to generate'''
+ parser = argparse.ArgumentParser(description="Generate ansible modules.")
+ parser.add_argument('--verify', action='store_true', default=False,
+ help='Verify library code matches the generated code.')
+
+ return parser.parse_args()
+
+
+def fragment_banner(fragment_path, side, data):
+ """Generate a banner to wrap around file fragments
+
+:param string fragment_path: A path to a module fragment
+:param string side: ONE OF: "header", "footer"
+:param StringIO data: A StringIO object to write the banner to
+"""
+ side_msg = {
+ "header": "Begin included fragment: {}",
+ "footer": "End included fragment: {}"
+ }
+ annotation = side_msg[side].format(fragment_path)
+
+ banner = """
+# -*- -*- -*- {} -*- -*- -*-
+""".format(annotation)
+
+ # Why skip?
+ #
+ # * 'generated' - This is the head of the script, we don't want to
+ # put comments before the #!shebang
+ #
+ # * 'license' - Wrapping this just seemed like gratuitous extra
+ if ("generated" not in fragment_path) and ("license" not in fragment_path):
+ data.write(banner)
+
+ # Make it self-contained testable
+ return banner
+
+
+def generate(parts):
+ '''generate the source code for the ansible modules
+
+:param Array parts: An array of paths (strings) to module fragments
+ '''
+
+ data = six.StringIO()
+ for fpart in parts:
+ # first line is pylint disable so skip it
+ with open(os.path.join(OPENSHIFT_ANSIBLE_PATH, fpart)) as pfd:
+ fragment_banner(fpart, "header", data)
+ for idx, line in enumerate(pfd):
+ if idx in [0, 1] and 'flake8: noqa' in line or 'pylint: skip-file' in line: # noqa: E501
+ continue
+
+ for skip in SKIP_COVERAGE_PATTERN:
+ if re.match(skip, line):
+ line = line.strip()
+ line += PRAGMA_STRING + os.linesep
+
+ data.write(line)
+
+ fragment_banner(fpart, "footer", data)
+ return data
+
+
+def get_sources():
+ '''return the path to the generate sources'''
+ return yaml.load(open(OPENSHIFT_ANSIBLE_SOURCES_PATH).read())
+
+
+def verify():
+ '''verify if the generated code matches the library code'''
+ for fname, parts in get_sources().items():
+ data = generate(parts)
+ fname = os.path.join(LIBRARY, fname)
+ if not open(fname).read() == data.getvalue():
+ raise GenerateAnsibleException('Generated content does not match for %s' % fname)
+
+
+def main():
+ ''' combine the necessary files to create the ansible module '''
+ args = parse_args()
+ if args.verify:
+ verify()
+
+ for fname, parts in get_sources().items():
+ data = generate(parts)
+ fname = os.path.join(LIBRARY, fname)
+ with open(fname, 'w') as afd:
+ afd.seek(0)
+ afd.write(data.getvalue())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/lib_openshift/src/lib/base.py b/roles/lib_openshift/src/lib/base.py
new file mode 100644
index 000000000..1fb32164e
--- /dev/null
+++ b/roles/lib_openshift/src/lib/base.py
@@ -0,0 +1,608 @@
+# pylint: skip-file
+# flake8: noqa
+# pylint: disable=too-many-lines
+# noqa: E301,E302,E303,T001
+
+
+class OpenShiftCLIError(Exception):
+ '''Exception class for openshiftcli'''
+ pass
+
+
+ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
+
+
+def locate_oc_binary():
+ ''' Find and return oc binary file '''
+ # https://github.com/openshift/openshift-ansible/issues/3410
+ # oc can be in /usr/local/bin in some cases, but that may not
+ # be in $PATH due to ansible/sudo
+ paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
+
+ oc_binary = 'oc'
+
+ # Use shutil.which if it is available, otherwise fallback to a naive path search
+ try:
+ which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
+ if which_result is not None:
+ oc_binary = which_result
+ except AttributeError:
+ for path in paths:
+ if os.path.exists(os.path.join(path, oc_binary)):
+ oc_binary = os.path.join(path, oc_binary)
+ break
+
+ return oc_binary
+
+
+# pylint: disable=too-few-public-methods
+class OpenShiftCLI(object):
+ ''' Class to wrap the command line tools '''
+ def __init__(self,
+ namespace,
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
+ verbose=False,
+ all_namespaces=False):
+ ''' Constructor for OpenshiftCLI '''
+ self.namespace = namespace
+ self.verbose = verbose
+ self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
+ self.all_namespaces = all_namespaces
+ self.oc_binary = locate_oc_binary()
+
+ # Pylint allows only 5 arguments to be passed.
+ # pylint: disable=too-many-arguments
+ def _replace_content(self, resource, rname, content, force=False, sep='.'):
+ ''' replace the current object with the content '''
+ res = self._get(resource, rname)
+ if not res['results']:
+ return res
+
+ fname = Utils.create_tmpfile(rname + '-')
+
+ yed = Yedit(fname, res['results'][0], separator=sep)
+ changes = []
+ for key, value in content.items():
+ changes.append(yed.put(key, value))
+
+ if any([change[0] for change in changes]):
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._replace(fname, force)
+
+ return {'returncode': 0, 'updated': False}
+
+ def _replace(self, fname, force=False):
+ '''replace the current object with oc replace'''
+ # We are removing the 'resourceVersion' to handle
+ # a race condition when modifying oc objects
+ yed = Yedit(fname)
+ results = yed.delete('metadata.resourceVersion')
+ if results[0]:
+ yed.write()
+
+ cmd = ['replace', '-f', fname]
+ if force:
+ cmd.append('--force')
+ return self.openshift_cmd(cmd)
+
+ def _create_from_content(self, rname, content):
+ '''create a temporary file and then call oc create on it'''
+ fname = Utils.create_tmpfile(rname + '-')
+ yed = Yedit(fname, content=content)
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self._create(fname)
+
+ def _create(self, fname):
+ '''call oc create on a filename'''
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _delete(self, resource, name=None, selector=None):
+ '''call oc delete on a resource'''
+ cmd = ['delete', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+ else:
+ raise OpenShiftCLIError('Either name or selector is required when calling delete.')
+
+ return self.openshift_cmd(cmd)
+
+ def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
+ '''process a template
+
+ template_name: the name of the template to process
+ create: whether to send to oc create after processing
+ params: the parameters for the template
+ template_data: the incoming template's data; instead of a file
+ '''
+ cmd = ['process']
+ if template_data:
+ cmd.extend(['-f', '-'])
+ else:
+ cmd.append(template_name)
+ if params:
+ param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
+ cmd.append('-v')
+ cmd.extend(param_str)
+
+ results = self.openshift_cmd(cmd, output=True, input_data=template_data)
+
+ if results['returncode'] != 0 or not create:
+ return results
+
+ fname = Utils.create_tmpfile(template_name + '-')
+ yed = Yedit(fname, results['results'])
+ yed.write()
+
+ atexit.register(Utils.cleanup, [fname])
+
+ return self.openshift_cmd(['create', '-f', fname])
+
+ def _get(self, resource, name=None, selector=None):
+ '''return a resource by name '''
+ cmd = ['get', resource]
+ if selector is not None:
+ cmd.append('--selector={}'.format(selector))
+ elif name is not None:
+ cmd.append(name)
+
+ cmd.extend(['-o', 'json'])
+
+ rval = self.openshift_cmd(cmd, output=True)
+
+ # Ensure results are retuned in an array
+ if 'items' in rval:
+ rval['results'] = rval['items']
+ elif not isinstance(rval['results'], list):
+ rval['results'] = [rval['results']]
+
+ return rval
+
+ def _schedulable(self, node=None, selector=None, schedulable=True):
+ ''' perform oadm manage-node scheduable '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ cmd.append('--schedulable={}'.format(schedulable))
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
+
+ def _list_pods(self, node=None, selector=None, pod_selector=None):
+ ''' perform oadm list pods
+
+ node: the node in which to list pods
+ selector: the label selector filter if provided
+ pod_selector: the pod selector filter if provided
+ '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ cmd.extend(['--list-pods', '-o', 'json'])
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ # pylint: disable=too-many-arguments
+ def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
+ ''' perform oadm manage-node evacuate '''
+ cmd = ['manage-node']
+ if node:
+ cmd.extend(node)
+ else:
+ cmd.append('--selector={}'.format(selector))
+
+ if dry_run:
+ cmd.append('--dry-run')
+
+ if pod_selector:
+ cmd.append('--pod-selector={}'.format(pod_selector))
+
+ if grace_period:
+ cmd.append('--grace-period={}'.format(int(grace_period)))
+
+ if force:
+ cmd.append('--force')
+
+ cmd.append('--evacuate')
+
+ return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
+
+ def _version(self):
+ ''' return the openshift version'''
+ return self.openshift_cmd(['version'], output=True, output_type='raw')
+
+ def _import_image(self, url=None, name=None, tag=None):
+ ''' perform image import '''
+ cmd = ['import-image']
+
+ image = '{0}'.format(name)
+ if tag:
+ image += ':{0}'.format(tag)
+
+ cmd.append(image)
+
+ if url:
+ cmd.append('--from={0}/{1}'.format(url, image))
+
+ cmd.append('-n{0}'.format(self.namespace))
+
+ cmd.append('--confirm')
+ return self.openshift_cmd(cmd)
+
+ def _run(self, cmds, input_data):
+ ''' Actually executes the command. This makes mocking easier. '''
+ curr_env = os.environ.copy()
+ curr_env.update({'KUBECONFIG': self.kubeconfig})
+ proc = subprocess.Popen(cmds,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=curr_env)
+
+ stdout, stderr = proc.communicate(input_data)
+
+ return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
+
+ # pylint: disable=too-many-arguments,too-many-branches
+ def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
+ '''Base command for oc '''
+ cmds = [self.oc_binary]
+
+ if oadm:
+ cmds.append('adm')
+
+ cmds.extend(cmd)
+
+ if self.all_namespaces:
+ cmds.extend(['--all-namespaces'])
+ elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
+ cmds.extend(['-n', self.namespace])
+
+ if self.verbose:
+ print(' '.join(cmds))
+
+ try:
+ returncode, stdout, stderr = self._run(cmds, input_data)
+ except OSError as ex:
+ returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
+
+ rval = {"returncode": returncode,
+ "cmd": ' '.join(cmds)}
+
+ if output_type == 'json':
+ rval['results'] = {}
+ if output and stdout:
+ try:
+ rval['results'] = json.loads(stdout)
+ except ValueError as verr:
+ if "No JSON object could be decoded" in verr.args:
+ rval['err'] = verr.args
+ elif output_type == 'raw':
+ rval['results'] = stdout if output else ''
+
+ if self.verbose:
+ print("STDOUT: {0}".format(stdout))
+ print("STDERR: {0}".format(stderr))
+
+ if 'err' in rval or returncode != 0:
+ rval.update({"stderr": stderr,
+ "stdout": stdout})
+
+ return rval
+
+
+class Utils(object):
+ ''' utilities for openshiftcli modules '''
+
+ @staticmethod
+ def _write(filename, contents):
+ ''' Actually write the file contents to disk. This helps with mocking. '''
+
+ with open(filename, 'w') as sfd:
+ sfd.write(contents)
+
+ @staticmethod
+ def create_tmp_file_from_contents(rname, data, ftype='yaml'):
+ ''' create a file in tmp with name and contents'''
+
+ tmp = Utils.create_tmpfile(prefix=rname)
+
+ if ftype == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripDumper'):
+ Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
+ else:
+ Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
+
+ elif ftype == 'json':
+ Utils._write(tmp, json.dumps(data))
+ else:
+ Utils._write(tmp, data)
+
+ # Register cleanup when module is done
+ atexit.register(Utils.cleanup, [tmp])
+ return tmp
+
+ @staticmethod
+ def create_tmpfile_copy(inc_file):
+ '''create a temporary copy of a file'''
+ tmpfile = Utils.create_tmpfile('lib_openshift-')
+ Utils._write(tmpfile, open(inc_file).read())
+
+ # Cleanup the tmpfile
+ atexit.register(Utils.cleanup, [tmpfile])
+
+ return tmpfile
+
+ @staticmethod
+ def create_tmpfile(prefix='tmp'):
+ ''' Generates and returns a temporary file name '''
+
+ with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
+ return tmp.name
+
+ @staticmethod
+ def create_tmp_files_from_contents(content, content_type=None):
+ '''Turn an array of dict: filename, content into a files array'''
+ if not isinstance(content, list):
+ content = [content]
+ files = []
+ for item in content:
+ path = Utils.create_tmp_file_from_contents(item['path'] + '-',
+ item['data'],
+ ftype=content_type)
+ files.append({'name': os.path.basename(item['path']),
+ 'path': path})
+ return files
+
+ @staticmethod
+ def cleanup(files):
+ '''Clean up on exit '''
+ for sfile in files:
+ if os.path.exists(sfile):
+ if os.path.isdir(sfile):
+ shutil.rmtree(sfile)
+ elif os.path.isfile(sfile):
+ os.remove(sfile)
+
+ @staticmethod
+ def exists(results, _name):
+ ''' Check to see if the results include the name '''
+ if not results:
+ return False
+
+ if Utils.find_result(results, _name):
+ return True
+
+ return False
+
+ @staticmethod
+ def find_result(results, _name):
+ ''' Find the specified result by name'''
+ rval = None
+ for result in results:
+ if 'metadata' in result and result['metadata']['name'] == _name:
+ rval = result
+ break
+
+ return rval
+
+ @staticmethod
+ def get_resource_file(sfile, sfile_type='yaml'):
+ ''' return the service file '''
+ contents = None
+ with open(sfile) as sfd:
+ contents = sfd.read()
+
+ if sfile_type == 'yaml':
+ # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
+ # pylint: disable=no-member
+ if hasattr(yaml, 'RoundTripLoader'):
+ contents = yaml.load(contents, yaml.RoundTripLoader)
+ else:
+ contents = yaml.safe_load(contents)
+ elif sfile_type == 'json':
+ contents = json.loads(contents)
+
+ return contents
+
+ @staticmethod
+ def filter_versions(stdout):
+ ''' filter the oc version output '''
+
+ version_dict = {}
+ version_search = ['oc', 'openshift', 'kubernetes']
+
+ for line in stdout.strip().split('\n'):
+ for term in version_search:
+ if not line:
+ continue
+ if line.startswith(term):
+ version_dict[term] = line.split()[-1]
+
+ # horrible hack to get openshift version in Openshift 3.2
+ # By default "oc version in 3.2 does not return an "openshift" version
+ if "openshift" not in version_dict:
+ version_dict["openshift"] = version_dict["oc"]
+
+ return version_dict
+
+ @staticmethod
+ def add_custom_versions(versions):
+ ''' create custom versions strings '''
+
+ versions_dict = {}
+
+ for tech, version in versions.items():
+ # clean up "-" from version
+ if "-" in version:
+ version = version.split("-")[0]
+
+ if version.startswith('v'):
+ versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
+ # "v3.3.0.33" is what we have, we want "3.3"
+ versions_dict[tech + '_short'] = version[1:4]
+
+ return versions_dict
+
+ @staticmethod
+ def openshift_installed():
+ ''' check if openshift is installed '''
+ import rpm
+
+ transaction_set = rpm.TransactionSet()
+ rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
+
+ return rpmquery.count() > 0
+
+ # Disabling too-many-branches. This is a yaml dictionary comparison function
+ # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
+ @staticmethod
+ def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
+
+ # Currently these values are autogenerated and we do not need to check them
+ skip = ['metadata', 'status']
+ if skip_keys:
+ skip.extend(skip_keys)
+
+ for key, value in result_def.items():
+ if key in skip:
+ continue
+
+ # Both are lists
+ if isinstance(value, list):
+ if key not in user_def:
+ if debug:
+ print('User data does not have key [%s]' % key)
+ print('User data: %s' % user_def)
+ return False
+
+ if not isinstance(user_def[key], list):
+ if debug:
+ print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
+ return False
+
+ if len(user_def[key]) != len(value):
+ if debug:
+ print("List lengths are not equal.")
+ print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
+ print("user_def: %s" % user_def[key])
+ print("value: %s" % value)
+ return False
+
+ for values in zip(user_def[key], value):
+ if isinstance(values[0], dict) and isinstance(values[1], dict):
+ if debug:
+ print('sending list - list')
+ print(type(values[0]))
+ print(type(values[1]))
+ result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
+ if not result:
+ print('list compare returned false')
+ return False
+
+ elif value != user_def[key]:
+ if debug:
+ print('value should be identical')
+ print(user_def[key])
+ print(value)
+ return False
+
+ # recurse on a dictionary
+ elif isinstance(value, dict):
+ if key not in user_def:
+ if debug:
+ print("user_def does not have key [%s]" % key)
+ return False
+ if not isinstance(user_def[key], dict):
+ if debug:
+ print("dict returned false: not instance of dict")
+ return False
+
+ # before passing ensure keys match
+ api_values = set(value.keys()) - set(skip)
+ user_values = set(user_def[key].keys()) - set(skip)
+ if api_values != user_values:
+ if debug:
+ print("keys are not equal in dict")
+ print(user_values)
+ print(api_values)
+ return False
+
+ result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
+ if not result:
+ if debug:
+ print("dict returned false")
+ print(result)
+ return False
+
+ # Verify each key, value pair is the same
+ else:
+ if key not in user_def or value != user_def[key]:
+ if debug:
+ print("value not equal; user_def does not have key")
+ print(key)
+ print(value)
+ if key in user_def:
+ print(user_def[key])
+ return False
+
+ if debug:
+ print('returning true')
+ return True
+
+class OpenShiftCLIConfig(object):
+ '''Generic Config'''
+ def __init__(self, rname, namespace, kubeconfig, options):
+ self.kubeconfig = kubeconfig
+ self.name = rname
+ self.namespace = namespace
+ self._options = options
+
+ @property
+ def config_options(self):
+ ''' return config options '''
+ return self._options
+
+ def to_option_list(self, ascommalist=''):
+ '''return all options as a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs'''
+ return self.stringify(ascommalist)
+
+ def stringify(self, ascommalist=''):
+ ''' return the options hash as cli params in a string
+ if ascommalist is set to the name of a key, and
+ the value of that key is a dict, format the dict
+ as a list of comma delimited key=value pairs '''
+ rval = []
+ for key in sorted(self.config_options.keys()):
+ data = self.config_options[key]
+ if data['include'] \
+ and (data['value'] is not None or isinstance(data['value'], int)):
+ if key == ascommalist:
+ val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
+ else:
+ val = data['value']
+ rval.append('--{}={}'.format(key.replace('_', '-'), val))
+
+ return rval
+
diff --git a/roles/lib_openshift/src/lib/clusterrole.py b/roles/lib_openshift/src/lib/clusterrole.py
new file mode 100644
index 000000000..93ffababf
--- /dev/null
+++ b/roles/lib_openshift/src/lib/clusterrole.py
@@ -0,0 +1,68 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-public-methods
+class ClusterRole(Yedit):
+ ''' Class to model an openshift ClusterRole'''
+ rules_path = "rules"
+
+ def __init__(self, name=None, content=None):
+ ''' Constructor for clusterrole '''
+ if content is None:
+ content = ClusterRole.builder(name).yaml_dict
+
+ super(ClusterRole, self).__init__(content=content)
+
+ self.__rules = Rule.parse_rules(self.get(ClusterRole.rules_path)) or []
+
+ @property
+ def rules(self):
+ return self.__rules
+
+ @rules.setter
+ def rules(self, data):
+ self.__rules = data
+ self.put(ClusterRole.rules_path, self.__rules)
+
+ def rule_exists(self, inc_rule):
+ '''attempt to find the inc_rule in the rules list'''
+ for rule in self.rules:
+ if rule == inc_rule:
+ return True
+
+ return False
+
+ def compare(self, other, verbose=False):
+ '''compare function for clusterrole'''
+ for rule in other.rules:
+ if rule not in self.rules:
+ if verbose:
+ print('Rule in other not found in self. [{}]'.format(rule))
+ return False
+
+ for rule in self.rules:
+ if rule not in other.rules:
+ if verbose:
+ print('Rule in self not found in other. [{}]'.format(rule))
+ return False
+
+ return True
+
+ @staticmethod
+ def builder(name='default_clusterrole', rules=None):
+ '''return a clusterrole with name and/or rules'''
+ if rules is None:
+ rules = [{'apiGroups': [""],
+ 'attributeRestrictions': None,
+ 'verbs': [],
+ 'resources': []}]
+ content = {
+ 'apiVersion': 'v1',
+ 'kind': 'ClusterRole',
+ 'metadata': {'name': '{}'.format(name)},
+ 'rules': rules,
+ }
+
+ return ClusterRole(content=content)
+
diff --git a/roles/lib_openshift/src/lib/deploymentconfig.py b/roles/lib_openshift/src/lib/deploymentconfig.py
new file mode 100644
index 000000000..0549bba84
--- /dev/null
+++ b/roles/lib_openshift/src/lib/deploymentconfig.py
@@ -0,0 +1,350 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-public-methods
+class DeploymentConfig(Yedit):
+ ''' Class to model an openshift DeploymentConfig'''
+ default_deployment_config = '''
+apiVersion: v1
+kind: DeploymentConfig
+metadata:
+ name: default_dc
+ namespace: default
+spec:
+ replicas: 0
+ selector:
+ default_dc: default_dc
+ strategy:
+ resources: {}
+ rollingParams:
+ intervalSeconds: 1
+ maxSurge: 0
+ maxUnavailable: 25%
+ timeoutSeconds: 600
+ updatePercent: -25
+ updatePeriodSeconds: 1
+ type: Rolling
+ template:
+ metadata:
+ spec:
+ containers:
+ - env:
+ - name: default
+ value: default
+ image: default
+ imagePullPolicy: IfNotPresent
+ name: default_dc
+ ports:
+ - containerPort: 8000
+ hostPort: 8000
+ protocol: TCP
+ name: default_port
+ resources: {}
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ hostNetwork: true
+ nodeSelector:
+ type: compute
+ restartPolicy: Always
+ securityContext: {}
+ serviceAccount: default
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ triggers:
+ - type: ConfigChange
+'''
+
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content=None):
+ ''' Constructor for deploymentconfig '''
+ if not content:
+ content = DeploymentConfig.default_deployment_config
+
+ super(DeploymentConfig, self).__init__(content=content)
+
+ def add_env_value(self, key, value):
+ ''' add key, value pair to env array '''
+ rval = False
+ env = self.get_env_vars()
+ if env:
+ env.append({'name': key, 'value': value})
+ rval = True
+ else:
+ result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
+ rval = result[0]
+
+ return rval
+
+ def exists_env_value(self, key, value):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key and result['value'] == value:
+ return True
+
+ return False
+
+ def exists_env_key(self, key):
+ ''' return whether a key, value pair exists '''
+ results = self.get_env_vars()
+ if not results:
+ return False
+
+ for result in results:
+ if result['name'] == key:
+ return True
+
+ return False
+
+ def get_env_var(self, key):
+ '''return a environment variables '''
+ results = self.get(DeploymentConfig.env_path) or []
+ if not results:
+ return None
+
+ for env_var in results:
+ if env_var['name'] == key:
+ return env_var
+
+ return None
+
+ def get_env_vars(self):
+ '''return a environment variables '''
+ return self.get(DeploymentConfig.env_path) or []
+
+ def delete_env_var(self, keys):
+ '''delete a list of keys '''
+ if not isinstance(keys, list):
+ keys = [keys]
+
+ env_vars_array = self.get_env_vars()
+ modified = False
+ idx = None
+ for key in keys:
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ modified = True
+ del env_vars_array[idx]
+
+ if modified:
+ return True
+
+ return False
+
+ def update_env_var(self, key, value):
+ '''place an env in the env var list'''
+
+ env_vars_array = self.get_env_vars()
+ idx = None
+ for env_idx, env_var in enumerate(env_vars_array):
+ if env_var['name'] == key:
+ idx = env_idx
+ break
+
+ if idx:
+ env_vars_array[idx]['value'] = value
+ else:
+ self.add_env_value(key, value)
+
+ return True
+
+ def exists_volume_mount(self, volume_mount):
+ ''' return whether a volume mount exists '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts:
+ return False
+
+ volume_mount_found = False
+ for exist_volume_mount in exist_volume_mounts:
+ if exist_volume_mount['name'] == volume_mount['name']:
+ volume_mount_found = True
+ break
+
+ return volume_mount_found
+
+ def exists_volume(self, volume):
+ ''' return whether a volume exists '''
+ exist_volumes = self.get_volumes()
+
+ volume_found = False
+ for exist_volume in exist_volumes:
+ if exist_volume['name'] == volume['name']:
+ volume_found = True
+ break
+
+ return volume_found
+
+ def find_volume_by_name(self, volume, mounts=False):
+ ''' return the index of a volume '''
+ volumes = []
+ if mounts:
+ volumes = self.get_volume_mounts()
+ else:
+ volumes = self.get_volumes()
+ for exist_volume in volumes:
+ if exist_volume['name'] == volume['name']:
+ return exist_volume
+
+ return None
+
+ def get_replicas(self):
+ ''' return replicas setting '''
+ return self.get(DeploymentConfig.replicas_path)
+
+ def get_volume_mounts(self):
+ '''return volume mount information '''
+ return self.get_volumes(mounts=True)
+
+ def get_volumes(self, mounts=False):
+ '''return volume mount information '''
+ if mounts:
+ return self.get(DeploymentConfig.volume_mounts_path) or []
+
+ return self.get(DeploymentConfig.volumes_path) or []
+
+ def delete_volume_by_name(self, volume):
+ '''delete a volume '''
+ modified = False
+ exist_volume_mounts = self.get_volume_mounts()
+ exist_volumes = self.get_volumes()
+ del_idx = None
+ for idx, exist_volume in enumerate(exist_volumes):
+ if 'name' in exist_volume and exist_volume['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volumes[del_idx]
+ modified = True
+
+ del_idx = None
+ for idx, exist_volume_mount in enumerate(exist_volume_mounts):
+ if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
+ del_idx = idx
+ break
+
+ if del_idx != None:
+ del exist_volume_mounts[idx]
+ modified = True
+
+ return modified
+
+ def add_volume_mount(self, volume_mount):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not exist_volume_mounts and volume_mount:
+ self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
+ else:
+ exist_volume_mounts.append(volume_mount)
+
+ def add_volume(self, volume):
+ ''' add a volume or volume mount to the proper location '''
+ exist_volumes = self.get_volumes()
+ if not volume:
+ return
+
+ if not exist_volumes:
+ self.put(DeploymentConfig.volumes_path, [volume])
+ else:
+ exist_volumes.append(volume)
+
+ def update_replicas(self, replicas):
+ ''' update replicas value '''
+ self.put(DeploymentConfig.replicas_path, replicas)
+
+ def update_volume(self, volume):
+ '''place an env in the env var list'''
+ exist_volumes = self.get_volumes()
+
+ if not volume:
+ return False
+
+ # update the volume
+ update_idx = None
+ for idx, exist_vol in enumerate(exist_volumes):
+ if exist_vol['name'] == volume['name']:
+ update_idx = idx
+ break
+
+ if update_idx != None:
+ exist_volumes[update_idx] = volume
+ else:
+ self.add_volume(volume)
+
+ return True
+
+ def update_volume_mount(self, volume_mount):
+ '''place an env in the env var list'''
+ modified = False
+
+ exist_volume_mounts = self.get_volume_mounts()
+
+ if not volume_mount:
+ return False
+
+ # update the volume mount
+ for exist_vol_mount in exist_volume_mounts:
+ if exist_vol_mount['name'] == volume_mount['name']:
+ if 'mountPath' in exist_vol_mount and \
+ str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
+ exist_vol_mount['mountPath'] = volume_mount['mountPath']
+ modified = True
+ break
+
+ if not modified:
+ self.add_volume_mount(volume_mount)
+ modified = True
+
+ return modified
+
+ def needs_update_volume(self, volume, volume_mount):
+ ''' verify a volume update is needed '''
+ exist_volume = self.find_volume_by_name(volume)
+ exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
+ results = []
+ results.append(exist_volume['name'] == volume['name'])
+
+ if 'secret' in volume:
+ results.append('secret' in exist_volume)
+ results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
+ results.append(exist_volume_mount['name'] == volume_mount['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'emptyDir' in volume:
+ results.append(exist_volume_mount['name'] == volume['name'])
+ results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
+
+ elif 'persistentVolumeClaim' in volume:
+ pvc = 'persistentVolumeClaim'
+ results.append(pvc in exist_volume)
+ if results[-1]:
+ results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
+
+ if 'claimSize' in volume[pvc]:
+ results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
+
+ elif 'hostpath' in volume:
+ results.append('hostPath' in exist_volume)
+ results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
+
+ return not all(results)
+
+ def needs_update_replicas(self, replicas):
+ ''' verify whether a replica update is needed '''
+ current_reps = self.get(DeploymentConfig.replicas_path)
+ return not current_reps == replicas
diff --git a/roles/lib_openshift/src/lib/group.py b/roles/lib_openshift/src/lib/group.py
new file mode 100644
index 000000000..fac5fcbc2
--- /dev/null
+++ b/roles/lib_openshift/src/lib/group.py
@@ -0,0 +1,36 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class GroupConfig(object):
+ ''' Handle route options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig):
+ ''' constructor for handling group options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Group'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['users'] = None
+
+
+# pylint: disable=too-many-instance-attributes
+class Group(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'group'
+
+ def __init__(self, content):
+ '''Group constructor'''
+ super(Group, self).__init__(content=content)
diff --git a/roles/lib_openshift/src/lib/import.py b/roles/lib_openshift/src/lib/import.py
new file mode 100644
index 000000000..a79297898
--- /dev/null
+++ b/roles/lib_openshift/src/lib/import.py
@@ -0,0 +1,23 @@
+# pylint: skip-file
+# flake8: noqa
+'''
+ OpenShiftCLI class that wraps the oc commands in a subprocess
+'''
+# pylint: disable=too-many-lines
+
+from __future__ import print_function
+import atexit
+import copy
+import json
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+# pylint: disable=import-error
+try:
+ import ruamel.yaml as yaml
+except ImportError:
+ import yaml
+
+from ansible.module_utils.basic import AnsibleModule
diff --git a/roles/lib_openshift/src/lib/project.py b/roles/lib_openshift/src/lib/project.py
new file mode 100644
index 000000000..40994741c
--- /dev/null
+++ b/roles/lib_openshift/src/lib/project.py
@@ -0,0 +1,85 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class ProjectConfig(OpenShiftCLIConfig):
+ ''' project config object '''
+ def __init__(self, rname, namespace, kubeconfig, project_options):
+ super(ProjectConfig, self).__init__(rname, None, kubeconfig, project_options)
+
+
+class Project(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ annotations_path = "metadata.annotations"
+ kind = 'Project'
+ annotation_prefix = 'openshift.io/'
+
+ def __init__(self, content):
+ '''Project constructor'''
+ super(Project, self).__init__(content=content)
+
+ def get_annotations(self):
+ ''' return the annotations'''
+ return self.get(Project.annotations_path) or {}
+
+ def add_annotations(self, inc_annos):
+ ''' add an annotation to the other annotations'''
+ if not isinstance(inc_annos, list):
+ inc_annos = [inc_annos]
+
+ annos = self.get_annotations()
+ if not annos:
+ self.put(Project.annotations_path, inc_annos)
+ else:
+ for anno in inc_annos:
+ for key, value in anno.items():
+ annos[key] = value
+
+ return True
+
+ def find_annotation(self, key):
+ ''' find an annotation'''
+ annotations = self.get_annotations()
+ for anno in annotations:
+ if Project.annotation_prefix + key == anno:
+ return annotations[anno]
+
+ return None
+
+ def delete_annotation(self, inc_anno_keys):
+ ''' remove an annotation from a project'''
+ if not isinstance(inc_anno_keys, list):
+ inc_anno_keys = [inc_anno_keys]
+
+ annos = self.get(Project.annotations_path) or {}
+
+ if not annos:
+ return True
+
+ removed = False
+ for inc_anno in inc_anno_keys:
+ anno = self.find_annotation(inc_anno)
+ if anno:
+ del annos[Project.annotation_prefix + anno]
+ removed = True
+
+ return removed
+
+ def update_annotation(self, key, value):
+ ''' remove an annotation for a project'''
+ annos = self.get(Project.annotations_path) or {}
+
+ if not annos:
+ return True
+
+ updated = False
+ anno = self.find_annotation(key)
+ if anno:
+ annos[Project.annotation_prefix + key] = value
+ updated = True
+
+ else:
+ self.add_annotations({Project.annotation_prefix + key: value})
+
+ return updated
diff --git a/roles/lib_openshift/src/lib/pvc.py b/roles/lib_openshift/src/lib/pvc.py
new file mode 100644
index 000000000..d1e935c32
--- /dev/null
+++ b/roles/lib_openshift/src/lib/pvc.py
@@ -0,0 +1,217 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class PersistentVolumeClaimConfig(object):
+ ''' Handle pvc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ access_modes=None,
+ vol_capacity='1G',
+ selector=None,
+ storage_class_name=None):
+ ''' constructor for handling pvc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.access_modes = access_modes
+ self.vol_capacity = vol_capacity
+ self.data = {}
+ self.selector = selector
+ self.storage_class_name = storage_class_name
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'PersistentVolumeClaim'
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ # spec
+ self.data['spec'] = {}
+ self.data['spec']['accessModes'] = ['ReadWriteOnce']
+ if self.access_modes:
+ self.data['spec']['accessModes'] = self.access_modes
+ if self.selector:
+ self.data['spec']['selector'] = {'matchLabels': self.selector}
+
+ # storage capacity
+ self.data['spec']['resources'] = {}
+ self.data['spec']['resources']['requests'] = {}
+ self.data['spec']['resources']['requests']['storage'] = self.vol_capacity
+
+ if self.storage_class_name:
+ self.data['spec']['storageClassName'] = self.storage_class_name
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class PersistentVolumeClaim(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ access_modes_path = "spec.accessModes"
+ volume_capacity_path = "spec.requests.storage"
+ volume_name_path = "spec.volumeName"
+ bound_path = "status.phase"
+ kind = 'PersistentVolumeClaim'
+ selector_path = "spec.selector.matchLabels"
+ storage_class_name_path = "spec.storageClassName"
+
+ def __init__(self, content):
+ '''PersistentVolumeClaim constructor'''
+ super(PersistentVolumeClaim, self).__init__(content=content)
+ self._access_modes = None
+ self._volume_capacity = None
+ self._volume_name = None
+ self._selector = None
+ self._storage_class_name = None
+
+ @property
+ def storage_class_name(self):
+ ''' storage_class_name property '''
+ if self._storage_class_name is None:
+ self._storage_class_name = self.get_storage_class_name()
+ return self._storage_class_name
+
+ @storage_class_name.setter
+ def storage_class_name(self, data):
+ ''' storage_class_name property setter'''
+ self._storage_class_name = data
+
+ @property
+ def volume_name(self):
+ ''' volume_name property '''
+ if self._volume_name is None:
+ self._volume_name = self.get_volume_name()
+ return self._volume_name
+
+ @volume_name.setter
+ def volume_name(self, data):
+ ''' volume_name property setter'''
+ self._volume_name = data
+
+ @property
+ def selector(self):
+ ''' selector property '''
+ if self._selector is None:
+ self._selector = self.get_selector()
+ if not isinstance(self._selector, dict):
+ self._selector = dict(self._selector)
+
+ return self._selector
+
+ @selector.setter
+ def selector(self, data):
+ ''' selector property setter'''
+ if not isinstance(data, dict):
+ data = dict(data)
+
+ self._selector = data
+
+ @property
+ def access_modes(self):
+ ''' access_modes property '''
+ if self._access_modes is None:
+ self._access_modes = self.get_access_modes()
+ if not isinstance(self._access_modes, list):
+ self._access_modes = list(self._access_modes)
+
+ return self._access_modes
+
+ @access_modes.setter
+ def access_modes(self, data):
+ ''' access_modes property setter'''
+ if not isinstance(data, list):
+ data = list(data)
+
+ self._access_modes = data
+
+ @property
+ def volume_capacity(self):
+ ''' volume_capacity property '''
+ if self._volume_capacity is None:
+ self._volume_capacity = self.get_volume_capacity()
+ return self._volume_capacity
+
+ @volume_capacity.setter
+ def volume_capacity(self, data):
+ ''' volume_capacity property setter'''
+ self._volume_capacity = data
+
+ def get_storage_class_name(self):
+ '''get storage_class_name'''
+ return self.get(PersistentVolumeClaim.storage_class_name_path) or []
+
+ def get_selector(self):
+ '''get selector'''
+ return self.get(PersistentVolumeClaim.selector_path) or []
+
+ def get_access_modes(self):
+ '''get access_modes'''
+ return self.get(PersistentVolumeClaim.access_modes_path) or []
+
+ def get_volume_capacity(self):
+ '''get volume_capacity'''
+ return self.get(PersistentVolumeClaim.volume_capacity_path) or []
+
+ def get_volume_name(self):
+ '''get volume_name'''
+ return self.get(PersistentVolumeClaim.volume_name_path) or []
+
+ def is_bound(self):
+ '''return whether volume is bound'''
+ return self.get(PersistentVolumeClaim.bound_path) or []
+
+ #### ADD #####
+ def add_access_mode(self, inc_mode):
+ ''' add an access_mode'''
+ if self.access_modes:
+ self.access_modes.append(inc_mode)
+ else:
+ self.put(PersistentVolumeClaim.access_modes_path, [inc_mode])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_access_mode(self, inc_mode):
+ ''' remove an access_mode'''
+ try:
+ self.access_modes.remove(inc_mode)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_access_mode(self, inc_mode):
+ ''' update an access_mode'''
+ try:
+ index = self.access_modes.index(inc_mode)
+ except ValueError as _:
+ return self.add_access_mode(inc_mode)
+
+ self.access_modes[index] = inc_mode
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_access_mode(self, inc_mode):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.access_modes.index(inc_mode)
+ except ValueError as _:
+ return index
+
+ return index
diff --git a/roles/lib_openshift/src/lib/replicationcontroller.py b/roles/lib_openshift/src/lib/replicationcontroller.py
new file mode 100644
index 000000000..8bcc1e3cc
--- /dev/null
+++ b/roles/lib_openshift/src/lib/replicationcontroller.py
@@ -0,0 +1,21 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-public-methods
+class ReplicationController(DeploymentConfig):
+ ''' Class to model a replicationcontroller openshift object.
+
+ Currently we are modeled after a deployment config since they
+ are very similar. In the future, when the need arises we
+ will add functionality to this class.
+ '''
+ replicas_path = "spec.replicas"
+ env_path = "spec.template.spec.containers[0].env"
+ volumes_path = "spec.template.spec.volumes"
+ container_path = "spec.template.spec.containers"
+ volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
+
+ def __init__(self, content):
+ ''' Constructor for ReplicationController '''
+ super(ReplicationController, self).__init__(content=content)
diff --git a/roles/lib_openshift/src/lib/rolebinding.py b/roles/lib_openshift/src/lib/rolebinding.py
new file mode 100644
index 000000000..69629f9f5
--- /dev/null
+++ b/roles/lib_openshift/src/lib/rolebinding.py
@@ -0,0 +1,289 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class RoleBindingConfig(object):
+ ''' Handle rolebinding config '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ namespace,
+ kubeconfig,
+ group_names=None,
+ role_ref=None,
+ subjects=None,
+ usernames=None):
+ ''' constructor for handling rolebinding options '''
+ self.kubeconfig = kubeconfig
+ self.name = name
+ self.namespace = namespace
+ self.group_names = group_names
+ self.role_ref = role_ref
+ self.subjects = subjects
+ self.usernames = usernames
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' create a default rolebinding as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'RoleBinding'
+ self.data['groupNames'] = self.group_names
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['roleRef'] = self.role_ref
+ self.data['subjects'] = self.subjects
+ self.data['userNames'] = self.usernames
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class RoleBinding(Yedit):
+ ''' Class to model a rolebinding openshift object'''
+ group_names_path = "groupNames"
+ role_ref_path = "roleRef"
+ subjects_path = "subjects"
+ user_names_path = "userNames"
+
+ kind = 'RoleBinding'
+
+ def __init__(self, content):
+ '''RoleBinding constructor'''
+ super(RoleBinding, self).__init__(content=content)
+ self._subjects = None
+ self._role_ref = None
+ self._group_names = None
+ self._user_names = None
+
+ @property
+ def subjects(self):
+ ''' subjects property '''
+ if self._subjects is None:
+ self._subjects = self.get_subjects()
+ return self._subjects
+
+ @subjects.setter
+ def subjects(self, data):
+ ''' subjects property setter'''
+ self._subjects = data
+
+ @property
+ def role_ref(self):
+ ''' role_ref property '''
+ if self._role_ref is None:
+ self._role_ref = self.get_role_ref()
+ return self._role_ref
+
+ @role_ref.setter
+ def role_ref(self, data):
+ ''' role_ref property setter'''
+ self._role_ref = data
+
+ @property
+ def group_names(self):
+ ''' group_names property '''
+ if self._group_names is None:
+ self._group_names = self.get_group_names()
+ return self._group_names
+
+ @group_names.setter
+ def group_names(self, data):
+ ''' group_names property setter'''
+ self._group_names = data
+
+ @property
+ def user_names(self):
+ ''' user_names property '''
+ if self._user_names is None:
+ self._user_names = self.get_user_names()
+ return self._user_names
+
+ @user_names.setter
+ def user_names(self, data):
+ ''' user_names property setter'''
+ self._user_names = data
+
+ def get_group_names(self):
+ ''' return groupNames '''
+ return self.get(RoleBinding.group_names_path) or []
+
+ def get_user_names(self):
+ ''' return usernames '''
+ return self.get(RoleBinding.user_names_path) or []
+
+ def get_role_ref(self):
+ ''' return role_ref '''
+ return self.get(RoleBinding.role_ref_path) or {}
+
+ def get_subjects(self):
+ ''' return subjects '''
+ return self.get(RoleBinding.subjects_path) or []
+
+ #### ADD #####
+ def add_subject(self, inc_subject):
+ ''' add a subject '''
+ if self.subjects:
+ # pylint: disable=no-member
+ self.subjects.append(inc_subject)
+ else:
+ self.put(RoleBinding.subjects_path, [inc_subject])
+
+ return True
+
+ def add_role_ref(self, inc_role_ref):
+ ''' add a role_ref '''
+ if not self.role_ref:
+ self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
+ return True
+
+ return False
+
+ def add_group_names(self, inc_group_names):
+ ''' add a group_names '''
+ if self.group_names:
+ # pylint: disable=no-member
+ self.group_names.append(inc_group_names)
+ else:
+ self.put(RoleBinding.group_names_path, [inc_group_names])
+
+ return True
+
+ def add_user_name(self, inc_user_name):
+ ''' add a username '''
+ if self.user_names:
+ # pylint: disable=no-member
+ self.user_names.append(inc_user_name)
+ else:
+ self.put(RoleBinding.user_names_path, [inc_user_name])
+
+ return True
+
+ #### /ADD #####
+
+ #### Remove #####
+ def remove_subject(self, inc_subject):
+ ''' remove a subject '''
+ try:
+ # pylint: disable=no-member
+ self.subjects.remove(inc_subject)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_role_ref(self, inc_role_ref):
+ ''' remove a role_ref '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref:
+ del self.role_ref['name']
+ return True
+
+ return False
+
+ def remove_group_name(self, inc_group_name):
+ ''' remove a groupname '''
+ try:
+ # pylint: disable=no-member
+ self.group_names.remove(inc_group_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_user_name(self, inc_user_name):
+ ''' remove a username '''
+ try:
+ # pylint: disable=no-member
+ self.user_names.remove(inc_user_name)
+ except ValueError as _:
+ return False
+
+ return True
+
+ #### /REMOVE #####
+
+ #### UPDATE #####
+ def update_subject(self, inc_subject):
+ ''' update a subject '''
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return self.add_subject(inc_subject)
+
+ self.subjects[index] = inc_subject
+
+ return True
+
+ def update_group_name(self, inc_group_name):
+ ''' update a groupname '''
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return self.add_group_names(inc_group_name)
+
+ self.group_names[index] = inc_group_name
+
+ return True
+
+ def update_user_name(self, inc_user_name):
+ ''' update a username '''
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return self.add_user_name(inc_user_name)
+
+ self.user_names[index] = inc_user_name
+
+ return True
+
+ def update_role_ref(self, inc_role_ref):
+ ''' update a role_ref '''
+ self.role_ref['name'] = inc_role_ref
+
+ return True
+
+ #### /UPDATE #####
+
+ #### FIND ####
+ def find_subject(self, inc_subject):
+ ''' find a subject '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.subjects.index(inc_subject)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group_name(self, inc_group_name):
+ ''' find a group_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.group_names.index(inc_group_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_user_name(self, inc_user_name):
+ ''' find a user_name '''
+ index = None
+ try:
+ # pylint: disable=no-member
+ index = self.user_names.index(inc_user_name)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_role_ref(self, inc_role_ref):
+ ''' find a user_name '''
+ if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
+ return self.role_ref
+
+ return None
diff --git a/roles/lib_openshift/src/lib/route.py b/roles/lib_openshift/src/lib/route.py
new file mode 100644
index 000000000..b106866cb
--- /dev/null
+++ b/roles/lib_openshift/src/lib/route.py
@@ -0,0 +1,141 @@
+# pylint: skip-file
+# flake8: noqa
+# noqa: E302,E301
+
+
+# pylint: disable=too-many-instance-attributes
+class RouteConfig(object):
+ ''' Handle route options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ labels=None,
+ destcacert=None,
+ cacert=None,
+ cert=None,
+ key=None,
+ host=None,
+ tls_termination=None,
+ service_name=None,
+ wildcard_policy=None,
+ weight=None,
+ port=None):
+ ''' constructor for handling route options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.namespace = namespace
+ self.labels = labels
+ self.host = host
+ self.tls_termination = tls_termination
+ self.destcacert = destcacert
+ self.cacert = cacert
+ self.cert = cert
+ self.key = key
+ self.service_name = service_name
+ self.port = port
+ self.data = {}
+ self.wildcard_policy = wildcard_policy
+ if wildcard_policy is None:
+ self.wildcard_policy = 'None'
+ self.weight = weight
+ if weight is None:
+ self.weight = 100
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a service as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Route'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ if self.labels:
+ self.data['metadata']['labels'] = self.labels
+ self.data['spec'] = {}
+
+ self.data['spec']['host'] = self.host
+
+ if self.tls_termination:
+ self.data['spec']['tls'] = {}
+
+ self.data['spec']['tls']['termination'] = self.tls_termination
+
+ if self.tls_termination != 'passthrough':
+ self.data['spec']['tls']['key'] = self.key
+ self.data['spec']['tls']['caCertificate'] = self.cacert
+ self.data['spec']['tls']['certificate'] = self.cert
+
+ if self.tls_termination == 'reencrypt':
+ self.data['spec']['tls']['destinationCACertificate'] = self.destcacert
+
+ self.data['spec']['to'] = {'kind': 'Service',
+ 'name': self.service_name,
+ 'weight': self.weight}
+
+ self.data['spec']['wildcardPolicy'] = self.wildcard_policy
+
+ if self.port:
+ self.data['spec']['port'] = {}
+ self.data['spec']['port']['targetPort'] = self.port
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class Route(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ wildcard_policy = "spec.wildcardPolicy"
+ host_path = "spec.host"
+ port_path = "spec.port.targetPort"
+ service_path = "spec.to.name"
+ weight_path = "spec.to.weight"
+ cert_path = "spec.tls.certificate"
+ cacert_path = "spec.tls.caCertificate"
+ destcacert_path = "spec.tls.destinationCACertificate"
+ termination_path = "spec.tls.termination"
+ key_path = "spec.tls.key"
+ kind = 'route'
+
+ def __init__(self, content):
+ '''Route constructor'''
+ super(Route, self).__init__(content=content)
+
+ def get_destcacert(self):
+ ''' return cert '''
+ return self.get(Route.destcacert_path)
+
+ def get_cert(self):
+ ''' return cert '''
+ return self.get(Route.cert_path)
+
+ def get_key(self):
+ ''' return key '''
+ return self.get(Route.key_path)
+
+ def get_cacert(self):
+ ''' return cacert '''
+ return self.get(Route.cacert_path)
+
+ def get_service(self):
+ ''' return service name '''
+ return self.get(Route.service_path)
+
+ def get_weight(self):
+ ''' return service weight '''
+ return self.get(Route.weight_path)
+
+ def get_termination(self):
+ ''' return tls termination'''
+ return self.get(Route.termination_path)
+
+ def get_host(self):
+ ''' return host '''
+ return self.get(Route.host_path)
+
+ def get_port(self):
+ ''' return port '''
+ return self.get(Route.port_path)
+
+ def get_wildcard_policy(self):
+ ''' return wildcardPolicy '''
+ return self.get(Route.wildcard_policy)
diff --git a/roles/lib_openshift/src/lib/rule.py b/roles/lib_openshift/src/lib/rule.py
new file mode 100644
index 000000000..fe5ed9723
--- /dev/null
+++ b/roles/lib_openshift/src/lib/rule.py
@@ -0,0 +1,144 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class Rule(object):
+ '''class to represent a clusterrole rule
+
+ Example Rule Object's yaml:
+ - apiGroups:
+ - ""
+ attributeRestrictions: null
+ resources:
+ - persistentvolumes
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+
+ '''
+ def __init__(self,
+ api_groups=None,
+ attr_restrictions=None,
+ resources=None,
+ verbs=None):
+ self.__api_groups = api_groups if api_groups is not None else [""]
+ self.__verbs = verbs if verbs is not None else []
+ self.__resources = resources if resources is not None else []
+ self.__attribute_restrictions = attr_restrictions if attr_restrictions is not None else None
+
+ @property
+ def verbs(self):
+ '''property for verbs'''
+ if self.__verbs is None:
+ return []
+
+ return self.__verbs
+
+ @verbs.setter
+ def verbs(self, data):
+ '''setter for verbs'''
+ self.__verbs = data
+
+ @property
+ def api_groups(self):
+ '''property for api_groups'''
+ if self.__api_groups is None:
+ return []
+ return self.__api_groups
+
+ @api_groups.setter
+ def api_groups(self, data):
+ '''setter for api_groups'''
+ self.__api_groups = data
+
+ @property
+ def resources(self):
+ '''property for resources'''
+ if self.__resources is None:
+ return []
+
+ return self.__resources
+
+ @resources.setter
+ def resources(self, data):
+ '''setter for resources'''
+ self.__resources = data
+
+ @property
+ def attribute_restrictions(self):
+ '''property for attribute_restrictions'''
+ return self.__attribute_restrictions
+
+ @attribute_restrictions.setter
+ def attribute_restrictions(self, data):
+ '''setter for attribute_restrictions'''
+ self.__attribute_restrictions = data
+
+ def add_verb(self, inc_verb):
+ '''add a verb to the verbs array'''
+ self.verbs.append(inc_verb)
+
+ def add_api_group(self, inc_apigroup):
+ '''add an api_group to the api_groups array'''
+ self.api_groups.append(inc_apigroup)
+
+ def add_resource(self, inc_resource):
+ '''add an resource to the resources array'''
+ self.resources.append(inc_resource)
+
+ def remove_verb(self, inc_verb):
+ '''add a verb to the verbs array'''
+ try:
+ self.verbs.remove(inc_verb)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def remove_api_group(self, inc_api_group):
+ '''add a verb to the verbs array'''
+ try:
+ self.api_groups.remove(inc_api_group)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def remove_resource(self, inc_resource):
+ '''add a verb to the verbs array'''
+ try:
+ self.resources.remove(inc_resource)
+ return True
+ except ValueError:
+ pass
+
+ return False
+
+ def __eq__(self, other):
+ '''return whether rules are equal'''
+ return (self.attribute_restrictions == other.attribute_restrictions and
+ self.api_groups == other.api_groups and
+ self.resources == other.resources and
+ self.verbs == other.verbs)
+
+
+ @staticmethod
+ def parse_rules(inc_rules):
+ '''create rules from an array'''
+
+ results = []
+ for rule in inc_rules:
+ results.append(Rule(rule.get('apiGroups', ['']),
+ rule.get('attributeRestrictions', None),
+ rule.get('resources', []),
+ rule.get('verbs', [])))
+
+ return results
diff --git a/roles/lib_openshift/src/lib/scc.py b/roles/lib_openshift/src/lib/scc.py
new file mode 100644
index 000000000..3e2aa08d7
--- /dev/null
+++ b/roles/lib_openshift/src/lib/scc.py
@@ -0,0 +1,218 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class SecurityContextConstraintsConfig(object):
+ ''' Handle scc options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ kubeconfig,
+ options=None,
+ fs_group='MustRunAs',
+ default_add_capabilities=None,
+ groups=None,
+ priority=None,
+ required_drop_capabilities=None,
+ run_as_user='MustRunAsRange',
+ se_linux_context='MustRunAs',
+ supplemental_groups='RunAsAny',
+ users=None,
+ annotations=None):
+ ''' constructor for handling scc options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.options = options
+ self.fs_group = fs_group
+ self.default_add_capabilities = default_add_capabilities
+ self.groups = groups
+ self.priority = priority
+ self.required_drop_capabilities = required_drop_capabilities
+ self.run_as_user = run_as_user
+ self.se_linux_context = se_linux_context
+ self.supplemental_groups = supplemental_groups
+ self.users = users
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a scc dict '''
+ # allow options
+ if self.options:
+ for key, value in self.options.items():
+ self.data[key] = value
+ else:
+ self.data['allowHostDirVolumePlugin'] = False
+ self.data['allowHostIPC'] = False
+ self.data['allowHostNetwork'] = False
+ self.data['allowHostPID'] = False
+ self.data['allowHostPorts'] = False
+ self.data['allowPrivilegedContainer'] = False
+ self.data['allowedCapabilities'] = None
+
+ # version
+ self.data['apiVersion'] = 'v1'
+ # kind
+ self.data['kind'] = 'SecurityContextConstraints'
+ # defaultAddCapabilities
+ self.data['defaultAddCapabilities'] = self.default_add_capabilities
+ # fsGroup
+ self.data['fsGroup']['type'] = self.fs_group
+ # groups
+ self.data['groups'] = []
+ if self.groups:
+ self.data['groups'] = self.groups
+ # metadata
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ if self.annotations:
+ for key, value in self.annotations.items():
+ self.data['metadata'][key] = value
+ # priority
+ self.data['priority'] = self.priority
+ # requiredDropCapabilities
+ self.data['requiredDropCapabilities'] = self.required_drop_capabilities
+ # runAsUser
+ self.data['runAsUser'] = {'type': self.run_as_user}
+ # seLinuxContext
+ self.data['seLinuxContext'] = {'type': self.se_linux_context}
+ # supplementalGroups
+ self.data['supplementalGroups'] = {'type': self.supplemental_groups}
+ # users
+ self.data['users'] = []
+ if self.users:
+ self.data['users'] = self.users
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods,no-member
+class SecurityContextConstraints(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ default_add_capabilities_path = "defaultAddCapabilities"
+ fs_group_path = "fsGroup"
+ groups_path = "groups"
+ priority_path = "priority"
+ required_drop_capabilities_path = "requiredDropCapabilities"
+ run_as_user_path = "runAsUser"
+ se_linux_context_path = "seLinuxContext"
+ supplemental_groups_path = "supplementalGroups"
+ users_path = "users"
+ kind = 'SecurityContextConstraints'
+
+ def __init__(self, content):
+ '''SecurityContextConstraints constructor'''
+ super(SecurityContextConstraints, self).__init__(content=content)
+ self._users = None
+ self._groups = None
+
+ @property
+ def users(self):
+ ''' users property getter '''
+ if self._users is None:
+ self._users = self.get_users()
+ return self._users
+
+ @property
+ def groups(self):
+ ''' groups property getter '''
+ if self._groups is None:
+ self._groups = self.get_groups()
+ return self._groups
+
+ @users.setter
+ def users(self, data):
+ ''' users property setter'''
+ self._users = data
+
+ @groups.setter
+ def groups(self, data):
+ ''' groups property setter'''
+ self._groups = data
+
+ def get_users(self):
+ '''get scc users'''
+ return self.get(SecurityContextConstraints.users_path) or []
+
+ def get_groups(self):
+ '''get scc groups'''
+ return self.get(SecurityContextConstraints.groups_path) or []
+
+ def add_user(self, inc_user):
+ ''' add a user '''
+ if self.users:
+ self.users.append(inc_user)
+ else:
+ self.put(SecurityContextConstraints.users_path, [inc_user])
+
+ return True
+
+ def add_group(self, inc_group):
+ ''' add a group '''
+ if self.groups:
+ self.groups.append(inc_group)
+ else:
+ self.put(SecurityContextConstraints.groups_path, [inc_group])
+
+ return True
+
+ def remove_user(self, inc_user):
+ ''' remove a user '''
+ try:
+ self.users.remove(inc_user)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def remove_group(self, inc_group):
+ ''' remove a group '''
+ try:
+ self.groups.remove(inc_group)
+ except ValueError as _:
+ return False
+
+ return True
+
+ def update_user(self, inc_user):
+ ''' update a user '''
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return self.add_user(inc_user)
+
+ self.users[index] = inc_user
+
+ return True
+
+ def update_group(self, inc_group):
+ ''' update a group '''
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return self.add_group(inc_group)
+
+ self.groups[index] = inc_group
+
+ return True
+
+ def find_user(self, inc_user):
+ ''' find a user '''
+ index = None
+ try:
+ index = self.users.index(inc_user)
+ except ValueError as _:
+ return index
+
+ return index
+
+ def find_group(self, inc_group):
+ ''' find a group '''
+ index = None
+ try:
+ index = self.groups.index(inc_group)
+ except ValueError as _:
+ return index
+
+ return index
diff --git a/roles/lib_openshift/src/lib/secret.py b/roles/lib_openshift/src/lib/secret.py
new file mode 100644
index 000000000..ad4b6aa36
--- /dev/null
+++ b/roles/lib_openshift/src/lib/secret.py
@@ -0,0 +1,105 @@
+# pylint: skip-file
+# flake8: noqa
+
+# pylint: disable=too-many-instance-attributes
+class SecretConfig(object):
+ ''' Handle secret options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ kubeconfig,
+ secrets=None,
+ stype=None,
+ annotations=None):
+ ''' constructor for handling secret options '''
+ self.kubeconfig = kubeconfig
+ self.name = sname
+ self.type = stype
+ self.namespace = namespace
+ self.secrets = secrets
+ self.annotations = annotations
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' assign the correct properties for a secret dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Secret'
+ self.data['type'] = self.type
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ self.data['data'] = {}
+ if self.secrets:
+ for key, value in self.secrets.items():
+ self.data['data'][key] = value
+ if self.annotations:
+ self.data['metadata']['annotations'] = self.annotations
+
+# pylint: disable=too-many-instance-attributes
+class Secret(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ secret_path = "data"
+ kind = 'secret'
+
+ def __init__(self, content):
+ '''secret constructor'''
+ super(Secret, self).__init__(content=content)
+ self._secrets = None
+
+ @property
+ def secrets(self):
+ '''secret property getter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self):
+ '''secret property setter'''
+ if self._secrets is None:
+ self._secrets = self.get_secrets()
+ return self._secrets
+
+ def get_secrets(self):
+ ''' returns all of the defined secrets '''
+ return self.get(Secret.secret_path) or {}
+
+ def add_secret(self, key, value):
+ ''' add a secret '''
+ if self.secrets:
+ self.secrets[key] = value
+ else:
+ self.put(Secret.secret_path, {key: value})
+
+ return True
+
+ def delete_secret(self, key):
+ ''' delete secret'''
+ try:
+ del self.secrets[key]
+ except KeyError as _:
+ return False
+
+ return True
+
+ def find_secret(self, key):
+ ''' find secret'''
+ rval = None
+ try:
+ rval = self.secrets[key]
+ except KeyError as _:
+ return None
+
+ return {'key': key, 'value': rval}
+
+ def update_secret(self, key, value):
+ ''' update a secret'''
+ if key in self.secrets:
+ self.secrets[key] = value
+ else:
+ self.add_secret(key, value)
+
+ return True
diff --git a/roles/lib_openshift/src/lib/service.py b/roles/lib_openshift/src/lib/service.py
new file mode 100644
index 000000000..0e8cc3aa5
--- /dev/null
+++ b/roles/lib_openshift/src/lib/service.py
@@ -0,0 +1,186 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class ServiceConfig(object):
+ ''' Handle service options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ sname,
+ namespace,
+ ports,
+ selector=None,
+ labels=None,
+ cluster_ip=None,
+ portal_ip=None,
+ session_affinity=None,
+ service_type=None,
+ external_ips=None):
+ ''' constructor for handling service options '''
+ self.name = sname
+ self.namespace = namespace
+ self.ports = ports
+ self.selector = selector
+ self.labels = labels
+ self.cluster_ip = cluster_ip
+ self.portal_ip = portal_ip
+ self.session_affinity = session_affinity
+ self.service_type = service_type
+ self.external_ips = external_ips
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiates a service dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'Service'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+ if self.labels:
+ self.data['metadata']['labels'] = {}
+ for lab, lab_value in self.labels.items():
+ self.data['metadata']['labels'][lab] = lab_value
+ self.data['spec'] = {}
+
+ if self.ports:
+ self.data['spec']['ports'] = self.ports
+ else:
+ self.data['spec']['ports'] = []
+
+ if self.selector:
+ self.data['spec']['selector'] = self.selector
+
+ self.data['spec']['sessionAffinity'] = self.session_affinity or 'None'
+
+ if self.cluster_ip:
+ self.data['spec']['clusterIP'] = self.cluster_ip
+
+ if self.portal_ip:
+ self.data['spec']['portalIP'] = self.portal_ip
+
+ if self.service_type:
+ self.data['spec']['type'] = self.service_type
+
+ if self.external_ips:
+ self.data['spec']['externalIPs'] = self.external_ips
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class Service(Yedit):
+ ''' Class to model the oc service object '''
+ port_path = "spec.ports"
+ portal_ip = "spec.portalIP"
+ cluster_ip = "spec.clusterIP"
+ selector_path = 'spec.selector'
+ kind = 'Service'
+ external_ips = "spec.externalIPs"
+
+ def __init__(self, content):
+ '''Service constructor'''
+ super(Service, self).__init__(content=content)
+
+ def get_ports(self):
+ ''' get a list of ports '''
+ return self.get(Service.port_path) or []
+
+ def get_selector(self):
+ ''' get the service selector'''
+ return self.get(Service.selector_path) or {}
+
+ def add_ports(self, inc_ports):
+ ''' add a port object to the ports list '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get_ports()
+ if not ports:
+ self.put(Service.port_path, inc_ports)
+ else:
+ ports.extend(inc_ports)
+
+ return True
+
+ def find_ports(self, inc_port):
+ ''' find a specific port '''
+ for port in self.get_ports():
+ if port['port'] == inc_port['port']:
+ return port
+
+ return None
+
+ def delete_ports(self, inc_ports):
+ ''' remove a port from a service '''
+ if not isinstance(inc_ports, list):
+ inc_ports = [inc_ports]
+
+ ports = self.get(Service.port_path) or []
+
+ if not ports:
+ return True
+
+ removed = False
+ for inc_port in inc_ports:
+ port = self.find_ports(inc_port)
+ if port:
+ ports.remove(port)
+ removed = True
+
+ return removed
+
+ def add_cluster_ip(self, sip):
+ '''add cluster ip'''
+ self.put(Service.cluster_ip, sip)
+
+ def add_portal_ip(self, pip):
+ '''add cluster ip'''
+ self.put(Service.portal_ip, pip)
+
+ def get_external_ips(self):
+ ''' get a list of external_ips '''
+ return self.get(Service.external_ips) or []
+
+ def add_external_ips(self, inc_external_ips):
+ ''' add an external_ip to the external_ips list '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get_external_ips()
+ if not external_ips:
+ self.put(Service.external_ips, inc_external_ips)
+ else:
+ external_ips.extend(inc_external_ips)
+
+ return True
+
+ def find_external_ips(self, inc_external_ip):
+ ''' find a specific external IP '''
+ val = None
+ try:
+ idx = self.get_external_ips().index(inc_external_ip)
+ val = self.get_external_ips()[idx]
+ except ValueError:
+ pass
+
+ return val
+
+ def delete_external_ips(self, inc_external_ips):
+ ''' remove an external IP from a service '''
+ if not isinstance(inc_external_ips, list):
+ inc_external_ips = [inc_external_ips]
+
+ external_ips = self.get(Service.external_ips) or []
+
+ if not external_ips:
+ return True
+
+ removed = False
+ for inc_external_ip in inc_external_ips:
+ external_ip = self.find_external_ips(inc_external_ip)
+ if external_ip:
+ external_ips.remove(external_ip)
+ removed = True
+
+ return removed
diff --git a/roles/lib_openshift/src/lib/serviceaccount.py b/roles/lib_openshift/src/lib/serviceaccount.py
new file mode 100644
index 000000000..50c104d44
--- /dev/null
+++ b/roles/lib_openshift/src/lib/serviceaccount.py
@@ -0,0 +1,129 @@
+# pylint: skip-file
+# flake8: noqa
+
+class ServiceAccountConfig(object):
+ '''Service account config class
+
+ This class stores the options and returns a default service account
+ '''
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None):
+ self.name = sname
+ self.kubeconfig = kubeconfig
+ self.namespace = namespace
+ self.secrets = secrets or []
+ self.image_pull_secrets = image_pull_secrets or []
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiate a properly structured volume '''
+ self.data['apiVersion'] = 'v1'
+ self.data['kind'] = 'ServiceAccount'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+ self.data['metadata']['namespace'] = self.namespace
+
+ self.data['secrets'] = []
+ if self.secrets:
+ for sec in self.secrets:
+ self.data['secrets'].append({"name": sec})
+
+ self.data['imagePullSecrets'] = []
+ if self.image_pull_secrets:
+ for sec in self.image_pull_secrets:
+ self.data['imagePullSecrets'].append({"name": sec})
+
+class ServiceAccount(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ image_pull_secrets_path = "imagePullSecrets"
+ secrets_path = "secrets"
+
+ def __init__(self, content):
+ '''ServiceAccount constructor'''
+ super(ServiceAccount, self).__init__(content=content)
+ self._secrets = None
+ self._image_pull_secrets = None
+
+ @property
+ def image_pull_secrets(self):
+ ''' property for image_pull_secrets '''
+ if self._image_pull_secrets is None:
+ self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or []
+ return self._image_pull_secrets
+
+ @image_pull_secrets.setter
+ def image_pull_secrets(self, secrets):
+ ''' property for secrets '''
+ self._image_pull_secrets = secrets
+
+ @property
+ def secrets(self):
+ ''' property for secrets '''
+ if not self._secrets:
+ self._secrets = self.get(ServiceAccount.secrets_path) or []
+ return self._secrets
+
+ @secrets.setter
+ def secrets(self, secrets):
+ ''' property for secrets '''
+ self._secrets = secrets
+
+ def delete_secret(self, inc_secret):
+ ''' remove a secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.secrets[remove_idx]
+ return True
+
+ return False
+
+ def delete_image_pull_secret(self, inc_secret):
+ ''' remove a image_pull_secret '''
+ remove_idx = None
+ for idx, sec in enumerate(self.image_pull_secrets):
+ if sec['name'] == inc_secret:
+ remove_idx = idx
+ break
+
+ if remove_idx:
+ del self.image_pull_secrets[remove_idx]
+ return True
+
+ return False
+
+ def find_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def find_image_pull_secret(self, inc_secret):
+ '''find secret'''
+ for secret in self.image_pull_secrets:
+ if secret['name'] == inc_secret:
+ return secret
+
+ return None
+
+ def add_secret(self, inc_secret):
+ '''add secret'''
+ if self.secrets:
+ self.secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.secrets_path, [{"name": inc_secret}])
+
+ def add_image_pull_secret(self, inc_secret):
+ '''add image_pull_secret'''
+ if self.image_pull_secrets:
+ self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member
+ else:
+ self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}])
diff --git a/roles/lib_openshift/src/lib/storageclass.py b/roles/lib_openshift/src/lib/storageclass.py
new file mode 100644
index 000000000..c49a3066a
--- /dev/null
+++ b/roles/lib_openshift/src/lib/storageclass.py
@@ -0,0 +1,73 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+# pylint: disable=too-many-instance-attributes
+class StorageClassConfig(object):
+ ''' Handle service options '''
+ # pylint: disable=too-many-arguments
+ def __init__(self,
+ name,
+ provisioner,
+ parameters=None,
+ annotations=None,
+ default_storage_class="false",
+ api_version='v1',
+ kubeconfig='/etc/origin/master/admin.kubeconfig'):
+ ''' constructor for handling storageclass options '''
+ self.name = name
+ self.parameters = parameters
+ self.annotations = annotations
+ self.provisioner = provisioner
+ self.api_version = api_version
+ self.default_storage_class = str(default_storage_class).lower()
+ self.kubeconfig = kubeconfig
+ self.data = {}
+
+ self.create_dict()
+
+ def create_dict(self):
+ ''' instantiates a storageclass dict '''
+ self.data['apiVersion'] = self.api_version
+ self.data['kind'] = 'StorageClass'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.name
+
+ self.data['metadata']['annotations'] = {}
+ if self.annotations is not None:
+ self.data['metadata']['annotations'] = self.annotations
+
+ self.data['metadata']['annotations']['storageclass.beta.kubernetes.io/is-default-class'] = \
+ self.default_storage_class
+
+ self.data['provisioner'] = self.provisioner
+
+ self.data['parameters'] = {}
+ if self.parameters is not None:
+ self.data['parameters'].update(self.parameters)
+
+ # default to aws if no params were passed
+ else:
+ self.data['parameters']['type'] = 'gp2'
+
+
+
+# pylint: disable=too-many-instance-attributes,too-many-public-methods
+class StorageClass(Yedit):
+ ''' Class to model the oc storageclass object '''
+ annotations_path = "metadata.annotations"
+ provisioner_path = "provisioner"
+ parameters_path = "parameters"
+ kind = 'StorageClass'
+
+ def __init__(self, content):
+ '''StorageClass constructor'''
+ super(StorageClass, self).__init__(content=content)
+
+ def get_annotations(self):
+ ''' get a list of ports '''
+ return self.get(StorageClass.annotations_path) or {}
+
+ def get_parameters(self):
+ ''' get the service selector'''
+ return self.get(StorageClass.parameters_path) or {}
diff --git a/roles/lib_openshift/src/lib/user.py b/roles/lib_openshift/src/lib/user.py
new file mode 100644
index 000000000..a14d5fc91
--- /dev/null
+++ b/roles/lib_openshift/src/lib/user.py
@@ -0,0 +1,37 @@
+# pylint: skip-file
+# flake8: noqa
+
+
+class UserConfig(object):
+ ''' Handle user options '''
+ def __init__(self,
+ kubeconfig,
+ username,
+ full_name):
+ ''' constructor for handling user options '''
+ self.kubeconfig = kubeconfig
+ self.username = username
+ self.full_name = full_name
+
+ self.data = {}
+ self.create_dict()
+
+ def create_dict(self):
+ ''' return a user as a dict '''
+ self.data['apiVersion'] = 'v1'
+ self.data['fullName'] = self.full_name
+ self.data['groups'] = None
+ self.data['identities'] = None
+ self.data['kind'] = 'User'
+ self.data['metadata'] = {}
+ self.data['metadata']['name'] = self.username
+
+
+# pylint: disable=too-many-instance-attributes
+class User(Yedit):
+ ''' Class to wrap the oc command line tools '''
+ kind = 'user'
+
+ def __init__(self, content):
+ '''User constructor'''
+ super(User, self).__init__(content=content)
diff --git a/roles/lib_openshift/src/lib/volume.py b/roles/lib_openshift/src/lib/volume.py
new file mode 100644
index 000000000..c049c8b49
--- /dev/null
+++ b/roles/lib_openshift/src/lib/volume.py
@@ -0,0 +1,43 @@
+# pylint: skip-file
+# flake8: noqa
+
+class Volume(object):
+ ''' Class to represent an openshift volume object'''
+ volume_mounts_path = {"pod": "spec.containers[0].volumeMounts",
+ "dc": "spec.template.spec.containers[0].volumeMounts",
+ "rc": "spec.template.spec.containers[0].volumeMounts",
+ }
+ volumes_path = {"pod": "spec.volumes",
+ "dc": "spec.template.spec.volumes",
+ "rc": "spec.template.spec.volumes",
+ }
+
+ @staticmethod
+ def create_volume_structure(volume_info):
+ ''' return a properly structured volume '''
+ volume_mount = None
+ volume = {'name': volume_info['name']}
+ volume_type = volume_info['type'].lower()
+ if volume_type == 'secret':
+ volume['secret'] = {}
+ volume[volume_info['type']] = {'secretName': volume_info['secret_name']}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'emptydir':
+ volume['emptyDir'] = {}
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+ elif volume_type == 'pvc' or volume_type == 'persistentvolumeclaim':
+ volume['persistentVolumeClaim'] = {}
+ volume['persistentVolumeClaim']['claimName'] = volume_info['claimName']
+ volume['persistentVolumeClaim']['claimSize'] = volume_info['claimSize']
+ elif volume_type == 'hostpath':
+ volume['hostPath'] = {}
+ volume['hostPath']['path'] = volume_info['path']
+ elif volume_type == 'configmap':
+ volume['configMap'] = {}
+ volume['configMap']['name'] = volume_info['configmap_name']
+ volume_mount = {'mountPath': volume_info['path'],
+ 'name': volume_info['name']}
+
+ return (volume, volume_mount)
diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml
new file mode 100644
index 000000000..4636f6b9b
--- /dev/null
+++ b/roles/lib_openshift/src/sources.yml
@@ -0,0 +1,328 @@
+---
+oc_adm_ca_server_cert.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/ca_server_cert
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_adm_ca_server_cert.py
+- ansible/oc_adm_ca_server_cert.py
+
+oc_adm_csr.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/csr
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_adm_csr.py
+- ansible/oc_adm_csr.py
+
+oc_adm_manage_node.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/manage_node
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_adm_manage_node.py
+- ansible/oc_adm_manage_node.py
+
+oc_adm_policy_user.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/policy_user
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/rolebinding.py
+- lib/scc.py
+- class/oc_adm_policy_user.py
+- ansible/oc_adm_policy_user.py
+
+oc_adm_policy_group.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/policy_group
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/rolebinding.py
+- lib/scc.py
+- class/oc_adm_policy_group.py
+- ansible/oc_adm_policy_group.py
+
+oc_adm_registry.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/registry
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/deploymentconfig.py
+- lib/secret.py
+- lib/service.py
+- lib/volume.py
+- class/oc_version.py
+- class/oc_adm_registry.py
+- ansible/oc_adm_registry.py
+
+oc_adm_router.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/router
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/service.py
+- lib/deploymentconfig.py
+- lib/serviceaccount.py
+- lib/secret.py
+- lib/rolebinding.py
+- class/oc_adm_router.py
+- ansible/oc_adm_router.py
+
+oc_atomic_container.py:
+- doc/generated
+- doc/license
+- doc/atomic_container
+- ansible/oc_atomic_container.py
+
+oc_configmap.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/configmap
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_configmap.py
+- ansible/oc_configmap.py
+
+oc_clusterrole.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/clusterrole
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/rule.py
+- lib/clusterrole.py
+- class/oc_clusterrole.py
+- ansible/oc_clusterrole.py
+
+oc_edit.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/edit
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_edit.py
+- ansible/oc_edit.py
+
+oc_env.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/env
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/deploymentconfig.py
+- class/oc_env.py
+- ansible/oc_env.py
+
+
+oc_group.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/group
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/group.py
+- class/oc_group.py
+- ansible/oc_group.py
+
+oc_image.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/image
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_image.py
+- ansible/oc_image.py
+
+oc_label.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/label
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_label.py
+- ansible/oc_label.py
+
+oc_obj.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/obj
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_obj.py
+- ansible/oc_obj.py
+
+oc_process.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/process
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_process.py
+- ansible/oc_process.py
+
+oc_project.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/project
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/project.py
+- class/oc_project.py
+- ansible/oc_project.py
+
+oc_pvc.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/pvc
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/pvc.py
+- class/oc_pvc.py
+- ansible/oc_pvc.py
+
+oc_route.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/route
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/route.py
+- class/oc_route.py
+- ansible/oc_route.py
+
+oc_scale.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/scale
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/deploymentconfig.py
+- lib/replicationcontroller.py
+- class/oc_scale.py
+- ansible/oc_scale.py
+
+oc_secret.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/secret
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/secret.py
+- class/oc_secret.py
+- ansible/oc_secret.py
+
+oc_serviceaccount.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/serviceaccount
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/serviceaccount.py
+- class/oc_serviceaccount.py
+- ansible/oc_serviceaccount.py
+
+oc_serviceaccount_secret.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/serviceaccount_secret
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/serviceaccount.py
+- class/oc_serviceaccount_secret.py
+- ansible/oc_serviceaccount_secret.py
+
+oc_service.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/service
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/service.py
+- class/oc_service.py
+- ansible/oc_service.py
+
+oc_storageclass.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/storageclass
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/storageclass.py
+- class/oc_storageclass.py
+- ansible/oc_storageclass.py
+
+oc_user.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/user
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/user.py
+- class/oc_user.py
+- ansible/oc_user.py
+
+oc_version.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/version
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_version.py
+- ansible/oc_version.py
+
+oc_volume.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/volume
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- lib/deploymentconfig.py
+- lib/volume.py
+- class/oc_volume.py
+- ansible/oc_volume.py
+
+oc_objectvalidator.py:
+- doc/generated
+- doc/license
+- lib/import.py
+- doc/objectvalidator
+- ../../lib_utils/src/class/yedit.py
+- lib/base.py
+- class/oc_objectvalidator.py
+- ansible/oc_objectvalidator.py
diff --git a/roles/lib_openshift/src/test/generate-and-run-tests.sh b/roles/lib_openshift/src/test/generate-and-run-tests.sh
new file mode 100755
index 000000000..cd7571372
--- /dev/null
+++ b/roles/lib_openshift/src/test/generate-and-run-tests.sh
@@ -0,0 +1,54 @@
+#!/bin/bash -e
+
+
+if [ $# -ne 1 ] ; then
+ echo "Usage: $(basename $0) <master name>"
+ exit 1
+fi
+
+MASTER=$1
+
+
+
+# Put us in the same dir as the script.
+cd $(dirname $0)
+
+
+echo
+echo "Running lib_utils generate-and-run-tests.sh"
+echo "-------------------------------------------"
+../../../lib_utils/src/test/generate-and-run-tests.sh
+
+
+echo
+echo "Running lib_openshift generate"
+echo "------------------------------"
+../generate.py
+
+
+echo
+echo "Running lib_openshift Unit Tests"
+echo "----------------------------"
+cd unit
+
+for test in *.py; do
+ echo
+ echo "--------------------------------------------------------------------------------"
+ echo
+ echo "Running $test..."
+ ./$test
+done
+
+
+echo
+echo "Running lib_openshift Integration Tests"
+echo "-----------------------------------"
+cd ../integration
+
+for test in *.yml; do
+ echo
+ echo "--------------------------------------------------------------------------------"
+ echo
+ echo "Running $test..."
+ ./$test -vvv -e cli_master_test="$MASTER"
+done
diff --git a/roles/lib_openshift/src/test/integration/filter_plugins/test_filters.py b/roles/lib_openshift/src/test/integration/filter_plugins/test_filters.py
new file mode 100644
index 000000000..f350bd25d
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/filter_plugins/test_filters.py
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+'''
+Custom filters for use in testing
+'''
+
+
+class FilterModule(object):
+ ''' Custom filters for use in integration testing '''
+
+ @staticmethod
+ def label_dict_to_key_value_list(label_dict):
+ ''' Given a dict of labels/values, return list of key: <key> value: <value> pairs
+
+ These are only used in integration testing.
+ '''
+
+ label_list = []
+ for key in label_dict:
+ label_list.append({'key': key, 'value': label_dict[key]})
+
+ return label_list
+
+ def filters(self):
+ ''' returns a mapping of filters to methods '''
+ return {
+ "label_dict_to_key_value_list": self.label_dict_to_key_value_list,
+ }
diff --git a/roles/lib_openshift/src/test/integration/group.yml b/roles/lib_openshift/src/test/integration/group.yml
new file mode 100755
index 000000000..25aa5727b
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/group.yml
@@ -0,0 +1,229 @@
+#!/usr/bin/ansible-playbook
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+
+ vars:
+
+ post_tasks:
+ - name: delete test group (so future tests work)
+ oc_group:
+ state: absent
+ name: jgroup
+
+ - name: delete 2nd test group (so future tests work)
+ oc_group:
+ state: absent
+ name: jgroup2
+
+ - name: delete test user (so future tests work)
+ oc_user:
+ state: absent
+ username: jdiaz@redhat.com
+
+ - name: get group list
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert group 'jgroup' (test group) does not exist
+ assert:
+ that: group_out['results'][0] == {}
+
+ - name: get group list
+ oc_group:
+ state: list
+ name: jgroup2
+ register: group_out
+ #- debug: var=group_out
+ - name: assert group 'jgroup2' (test group) does not exist
+ assert:
+ that: group_out['results'][0] == {}
+
+ - name: get user list
+ oc_user:
+ state: list
+ username: 'jdiaz@redhat.com'
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user 'jdiaz@redhat.com' (test user) does not exist
+ assert:
+ that: group_out['results'][0] == {}
+
+ - name: create group
+ oc_group:
+ state: present
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert creating group marked changed
+ assert:
+ that: group_out['changed'] == True
+
+ - name: list group
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert group actually created
+ assert:
+ that: group_out['results'][0]['metadata']['name'] == 'jgroup'
+
+ - name: re-add group
+ oc_group:
+ state: present
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert re-adding group marked not changed
+ assert:
+ that: group_out['changed'] == False
+
+
+ - name: add user with group membership
+ oc_user:
+ state: present
+ username: jdiaz@redhat.com
+ full_name: Joel Diaz
+ groups:
+ - jgroup
+ register: group_out
+ #- debug: var=group_out
+
+ - name: get group
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ - name: assert user in group
+ assert:
+ that: group_out['results'][0]['users'][0] == 'jdiaz@redhat.com'
+
+ - name: add 2nd group
+ oc_group:
+ state: present
+ name: jgroup2
+
+ - name: change group membership
+ oc_user:
+ state: present
+ username: jdiaz@redhat.com
+ full_name: Joel Diaz
+ groups:
+ - jgroup2
+ register: group_out
+ - name: assert result changed
+ assert:
+ that: group_out['changed'] == True
+
+ - name: check jgroup user membership
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user not present in previous group
+ assert:
+ that: group_out['results'][0]['users'] == []
+
+ - name: check jgroup2 user membership
+ oc_group:
+ state: list
+ name: jgroup2
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user present in new group
+ assert:
+ that: group_out['results'][0]['users'][0] == 'jdiaz@redhat.com'
+
+ - name: multi-group membership
+ oc_user:
+ state: present
+ username: jdiaz@redhat.com
+ full_name: Joel Diaz
+ groups:
+ - jgroup
+ - jgroup2
+ register: group_out
+ - name: assert result changed
+ assert:
+ that: group_out['changed'] == True
+
+ - name: check jgroup user membership
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user present in group
+ assert:
+ that: group_out['results'][0]['users'][0] == 'jdiaz@redhat.com'
+
+ - name: check jgroup2 user membership
+ oc_group:
+ state: list
+ name: jgroup2
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user still present in group
+ assert:
+ that: group_out['results'][0]['users'][0] == 'jdiaz@redhat.com'
+
+ - name: user delete (group cleanup)
+ oc_user:
+ state: absent
+ username: jdiaz@redhat.com
+ register: group_out
+
+ - name: get user list for jgroup
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert that group jgroup has no members
+ assert:
+ that: group_out['results'][0]['users'] == []
+
+ - name: get user list for jgroup2
+ oc_group:
+ state: list
+ name: jgroup2
+ register: group_out
+ #- debug: var=group_out
+ - name: assert that group jgroup2 has no members
+ assert:
+ that: group_out['results'][0]['users'] == []
+
+ - name: user without groups defined
+ oc_user:
+ state: present
+ username: jdiaz@redhat.com
+ full_name: Joel Diaz
+ register: group_out
+ - name: assert result changed
+ assert:
+ that: group_out['changed'] == True
+
+ - name: check jgroup user membership
+ oc_group:
+ state: list
+ name: jgroup
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user not present in group
+ assert:
+ that: group_out['results'][0]['users'] == []
+
+ - name: check jgroup2 user membership
+ oc_group:
+ state: list
+ name: jgroup2
+ register: group_out
+ #- debug: var=group_out
+ - name: assert user not present in group
+ assert:
+ that: group_out['results'][0]['users'] == []
diff --git a/roles/lib_openshift/src/test/integration/oc_adm_csr.yml b/roles/lib_openshift/src/test/integration/oc_adm_csr.yml
new file mode 100755
index 000000000..cad8e36f5
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_adm_csr.yml
@@ -0,0 +1,28 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_adm_csr.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: masters
+ gather_facts: no
+ user: root
+ tasks:
+ - name: list csrs
+ oc_adm_csr:
+ state: list
+ register: csrout
+
+ - debug: var=csrout
+
+ - name: list csrs
+ oc_adm_csr:
+ state: approve
+ nodes:
+ - ip-172-31-51-0-ec2-internal
+ - ip-172-31-51-246-ec2-internal
+ - ip-172-31-54-12-ec2-internal
+ - ip-172-31-58-173-ec2-internal
+ - ip-172-31-58-212-ec2-internal
+ - ip-172-31-51-246-ec2-internal
+ - ip-172-31-54-12-ec2-internal
+
+ register: csrout
+ - debug: var=csrout
diff --git a/roles/lib_openshift/src/test/integration/oc_adm_manage_node.yml b/roles/lib_openshift/src/test/integration/oc_adm_manage_node.yml
new file mode 100755
index 000000000..1ed2ef11b
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_adm_manage_node.yml
@@ -0,0 +1,69 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+#
+# ./oc_adm_manage_node.yml -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: get list of nodes
+ oc_obj:
+ state: list
+ kind: node
+ register: obj_out
+
+ - name: Set the node to work with
+ set_fact:
+ node_to_test: "{{ obj_out['results']['results'][0]['items'][0]['metadata']['name'] }}"
+
+ - name: list pods from a node
+ oc_adm_manage_node:
+ list_pods: True
+ node:
+ - "{{ node_to_test }}"
+ register: podout
+ - debug: var=podout
+
+ - assert:
+ that: "'{{ node_to_test }}' in podout.results.nodes"
+ msg: Pod data was not returned
+
+ - name: set node to unschedulable
+ oc_adm_manage_node:
+ schedulable: False
+ node:
+ - "{{ node_to_test }}"
+ register: nodeout
+ - debug: var=nodeout
+
+ - name: assert that schedulable=False
+ assert:
+ that: nodeout.results.nodes[0]['schedulable'] == False
+ msg: "{{ node_to_test }} schedulable set to True"
+
+ - name: get node scheduable
+ oc_obj:
+ kind: node
+ state: list
+ name: "{{ node_to_test }}"
+ namespace: None
+ register: nodeout
+
+ - debug: var=nodeout
+
+ - name: assert that schedulable=False
+ assert:
+ that: nodeout.results.results[0]['spec']['unschedulable']
+
+ - name: set node to schedulable
+ oc_adm_manage_node:
+ schedulable: True
+ node:
+ - "{{ node_to_test }}"
+ register: nodeout
+ - debug: var=nodeout
+
+ - name: assert that schedulable=False
+ assert:
+ that: nodeout.results.nodes[0]['schedulable']
+ msg: "{{ node_to_test }} schedulable set to False"
diff --git a/roles/lib_openshift/src/test/integration/oc_clusterrole.yml b/roles/lib_openshift/src/test/integration/oc_clusterrole.yml
new file mode 100755
index 000000000..91b143f55
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_clusterrole.yml
@@ -0,0 +1,106 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+## ./oc_configmap.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+
+ post_tasks:
+ - name: create a test project
+ oc_project:
+ name: test
+ description: for tests only
+
+ ###### create test ###########
+ - name: create a clusterrole
+ oc_clusterrole:
+ state: present
+ name: operations
+ rules:
+ - apiGroups:
+ - ""
+ resources:
+ - persistentvolumes
+ attributeRestrictions: null
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+
+ - name: fetch the created clusterrole
+ oc_clusterrole:
+ name: operations
+ state: list
+ register: croleout
+
+ - debug: var=croleout
+
+ - name: assert clusterrole exists
+ assert:
+ that:
+ - croleout.results.results.metadata.name == 'operations'
+ - croleout.results.results.rules[0].resources[0] == 'persistentvolumes'
+ ###### end create test ###########
+
+ ###### update test ###########
+ - name: update a clusterrole
+ oc_clusterrole:
+ state: present
+ name: operations
+ rules:
+ - apiGroups:
+ - ""
+ resources:
+ - persistentvolumes
+ - serviceaccounts
+ - services
+ attributeRestrictions: null
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+
+ - name: fetch the created clusterrole
+ oc_clusterrole:
+ name: operations
+ state: list
+ register: croleout
+
+ - debug: var=croleout
+
+ - name: assert clusterrole is updated
+ assert:
+ that:
+ - croleout.results.results.metadata.name == 'operations'
+ - "'persistentvolumes' in croleout.results.results.rules[0].resources"
+ - "'serviceaccounts' in croleout.results.results.rules[0].resources"
+ - "'services' in croleout.results.results.rules[0].resources"
+ ###### end create test ###########
+
+ ###### delete test ###########
+ - name: delete a clusterrole
+ oc_clusterrole:
+ state: absent
+ name: operations
+
+ - name: fetch the clusterrole
+ oc_clusterrole:
+ name: operations
+ state: list
+ register: croleout
+
+ - debug: var=croleout
+
+ - name: assert operations does not exist
+ assert:
+ that: "'\"operations\" not found' in croleout.results.stderr"
diff --git a/roles/lib_openshift/src/test/integration/oc_configmap.yml b/roles/lib_openshift/src/test/integration/oc_configmap.yml
new file mode 100755
index 000000000..6a452ccec
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_configmap.yml
@@ -0,0 +1,95 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+## ./oc_configmap.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ vars:
+ filename: /tmp/test_configmap_from_file
+
+ post_tasks:
+ - name: Setup a file with known contents
+ copy:
+ content: This is a file
+ dest: "{{ filename }}"
+
+ - name: create a test project
+ oc_project:
+ name: test
+ description: for tests only
+
+ ###### create test ###########
+ - name: create a configmap
+ oc_configmap:
+ state: present
+ name: configmaptest
+ namespace: test
+ from_file:
+ config: "{{ filename }}"
+ from_literal:
+ foo: bar
+
+ - name: fetch the created configmap
+ oc_configmap:
+ name: configmaptest
+ state: list
+ namespace: test
+ register: cmout
+
+ - debug: var=cmout
+
+ - name: assert configmaptest exists
+ assert:
+ that:
+ - cmout.results.results[0].metadata.name == 'configmaptest'
+ - cmout.results.results[0].data.foo == 'bar'
+ ###### end create test ###########
+
+ ###### update test ###########
+ - name: create a configmap
+ oc_configmap:
+ state: present
+ name: configmaptest
+ namespace: test
+ from_file:
+ config: "{{ filename }}"
+ from_literal:
+ foo: notbar
+ deployment_type: openshift-enterprise
+
+ - name: fetch the updated configmap
+ oc_configmap:
+ name: configmaptest
+ state: list
+ namespace: test
+ register: cmout
+
+ - debug: var=cmout
+
+ - name: assert configmaptest exists
+ assert:
+ that:
+ - cmout.results.results[0].metadata.name == 'configmaptest'
+ - cmout.results.results[0].data.deployment_type == 'openshift-enterprise'
+ - cmout.results.results[0].data.foo == 'notbar'
+ ###### end update test ###########
+
+ ###### delete test ###########
+ - name: delete a configmap
+ oc_configmap:
+ state: absent
+ name: configmaptest
+ namespace: test
+
+ - name: fetch the updated configmap
+ oc_configmap:
+ name: configmaptest
+ state: list
+ namespace: test
+ register: cmout
+
+ - debug: var=cmout
+
+ - name: assert configmaptest exists
+ assert:
+ that: "'\"configmaptest\" not found' in cmout.results.stderr"
diff --git a/roles/lib_openshift/src/test/integration/oc_env.yml b/roles/lib_openshift/src/test/integration/oc_env.yml
new file mode 100755
index 000000000..cbb97ed46
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_env.yml
@@ -0,0 +1,75 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_env.yml -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ vars:
+ my_env_var:
+ SOMEKEY: SOMEVALUE
+
+ check_env_var:
+ name: DEFAULT_CERTIFICATE_DIR
+ value: /etc/pki/tls/private
+
+ tasks:
+ - name: list environment variables from router dc
+ oc_env:
+ state: list
+ name: router
+ namespace: default
+ kind: dc
+ register: envout
+ - debug: var=envout
+
+ - assert:
+ that:
+ - "'{{ check_env_var.name }}' == '{{ envout.results[0].name }}'"
+ - "{{ envout.results|length }} > 0"
+ msg: "Did not find environment variables."
+
+ - name: list environment variables from router dc
+ oc_env:
+ state: present
+ name: router
+ namespace: default
+ kind: dc
+ env_vars: "{{ my_env_var }}"
+ register: envout
+ - debug: var=envout
+
+ - assert:
+ that:
+ - "'SOMEKEY' == '{{ envout.results[-1].name }}'"
+ - "'SOMEVALUE' == '{{ envout.results[-1].value }}'"
+ msg: "Did not find updated environment variables."
+
+ - name: remove environment variables from router dc
+ oc_env:
+ state: absent
+ name: router
+ namespace: default
+ kind: dc
+ env_vars: "{{ my_env_var }}"
+ register: envout
+ - debug: var=envout
+
+ - assert:
+ that:
+ - envout.changed == True
+ msg: "state: Absent failed."
+
+ - name: list environment variables from router dc
+ oc_env:
+ state: list
+ name: router
+ namespace: default
+ kind: dc
+ register: envout
+ - debug: var=envout
+
+ - assert:
+ that:
+ - "'SOMEKEY' != '{{ envout.results[-1].name }}'"
+ - "'SOMEVALUE' != '{{ envout.results[-1].value }}'"
+ msg: "Did find updated environment variables."
diff --git a/roles/lib_openshift/src/test/integration/oc_label.yml b/roles/lib_openshift/src/test/integration/oc_label.yml
new file mode 100755
index 000000000..22cf687c5
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_label.yml
@@ -0,0 +1,334 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+#
+# ./oc_label.yml -e "cli_master_test=$OPENSHIFT_MASTER
+#
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+
+ vars:
+ - def_namespace: default
+ - def_kind: node
+
+ pre_tasks:
+ - name: ensure needed vars are defined
+ fail:
+ msg: "{{ item }} not defined"
+ when: item is not defined
+ with_items:
+ - cli_master_test # ansible inventory instance to run playbook against
+
+ tasks:
+ - name: get list of nodes
+ oc_obj:
+ state: list
+ kind: node
+ register: obj_out
+
+ - name: Set the node to work with
+ set_fact:
+ node_to_test: "{{ obj_out['results']['results'][0]['items'][0]['metadata']['name'] }}"
+
+ - name: delete test labels (start from known starting position)
+ oc_label:
+ state: absent
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ - key: testlabel3
+
+ - name: list to check whether our test labels already exist
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: original_labels
+ - name: assert that testlabel2 and testlabel3 test labels don't exist
+ assert:
+ that: original_labels['results']['labels'][0]['testlabel2'] is not defined and
+ original_labels['results']['labels'][0]['testlabel3'] is not defined
+ msg: "{{ original_labels['results']['labels'] }}"
+
+ - name: add label
+ oc_label:
+ state: add
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ value: "yes"
+ register: label_out
+ - name: assert adding label marked as changed
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: test if add label succeeded
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: assert that testlabel2 label actually added
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is defined and
+ label_out['results']['labels'][0]['testlabel2'] == "yes"
+ msg: "{{ label_out }}"
+
+ - name: test that re-adding does nothing
+ oc_label:
+ state: add
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ value: "yes"
+ register: label_out
+ - name: assert that re-adding made no changes
+ assert:
+ that: label_out['changed'] == False
+ msg: "{{ label_out }}"
+
+ - name: test that modifying existing label marked modified
+ oc_label:
+ state: add
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ value: "different"
+ register: label_out
+ - name: assert that modifying existing label marked modified
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: test if modify label actually did modification
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: assert that testlabel2 label actually modified
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is defined and
+ label_out['results']['labels'][0]['testlabel2'] == "different"
+ msg: "{{ label_out['results']['labels'] }}"
+
+ - name: delete non-existant label
+ oc_label:
+ state: absent
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabelnone
+ register: label_out
+ - name: assert that deleting non-existant label marked not changed
+ assert:
+ that: label_out['changed'] == False
+ msg: "{{ label_out }}"
+
+ - name: delete label
+ oc_label:
+ state: absent
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ register: label_out
+ - name: assert that deleting existing label marked changed
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: re-delete label
+ oc_label:
+ state: absent
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ register: label_out
+ - name: assert that re-deleting label marked not changed
+ assert:
+ that: label_out['changed'] == False
+ msg: "{{ label_out }}"
+
+ - name: check whether really deleted
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: assert label actually deleted
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is not defined
+ msg: "{{ label_out }}"
+
+ - name: add two labels
+ oc_label:
+ state: add
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ value: "yes"
+ - key: testlabel3
+ value: "yes"
+ register: label_out
+ - name: assert that adding two labels marked as changed
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: check whether both labels are there
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: assert that both labels actually exist
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is defined and
+ label_out['results']['labels'][0]['testlabel2'] == 'yes' and
+ label_out['results']['labels'][0]['testlabel3'] is defined and
+ label_out['results']['labels'][0]['testlabel3'] == 'yes'
+ msg: "{{ label_out['results']['labels'] }}"
+
+ - name: check whether two deletes work
+ oc_label:
+ state: absent
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ - key: testlabel3
+ register: label_out
+ - name: assert that change were made when delete both labels
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: check whether re-two deletes makes no changes
+ oc_label:
+ state: absent
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels:
+ - key: testlabel2
+ - key: testlabel3
+ register: label_out
+ - name: assert that change was not made when re-delete both labels
+ assert:
+ that: label_out['changed'] == False
+ msg: "{{ label_out }}"
+
+ - set_fact:
+ original_labels_as_key_value_list: "{{ original_labels['results']['labels'][0] | label_dict_to_key_value_list }}"
+
+ - name: check that present with original label list makes no changes
+ oc_label:
+ state: present
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels: "{{ original_labels_as_key_value_list }}"
+ register: label_out
+ - name: assert that no changes are made when current list matches existing list
+ assert:
+ that: label_out['changed'] == False
+ msg: "{{ label_out }}"
+
+ - name: check that present with extra item makes changes
+ oc_label:
+ state: present
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels: "{{ original_labels_as_key_value_list + [{'key': 'testlabel2', 'value': 'yes'}] }}"
+ register: label_out
+ - name: assert that changes were made
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: get current label list
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: asssert that new label was actually added
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is defined and
+ label_out['results']['labels'][0]['testlabel2'] == 'yes'
+ msg: "{{ label_out['results']['labels'] }}"
+
+ - name: check that present with changed item makes changes
+ oc_label:
+ state: present
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels: "{{ original_labels_as_key_value_list + [{'key': 'testlabel2', 'value': 'different'}]}}"
+ register: label_out
+ - name: assert that changes were made when existing key's value is changed
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: get current label list
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: asssert that changed label was actually changed
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is defined and
+ label_out['results']['labels'][0]['testlabel2'] == 'different'
+ msg: "{{ label_out['results']['labels'] }}"
+
+ - name: check that present with removed extra item makes changes
+ oc_label:
+ state: present
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ labels: "{{ original_labels_as_key_value_list }}"
+ register: label_out
+ - name: assert that changes were made
+ assert:
+ that: label_out['changed'] == True
+ msg: "{{ label_out }}"
+
+ - name: get current label list
+ oc_label:
+ state: list
+ namespace: "{{ def_namespace }}"
+ kind: "{{ def_kind }}"
+ name: "{{ node_to_test }}"
+ register: label_out
+ - name: asssert that present-removed actually removed
+ assert:
+ that: label_out['results']['labels'][0]['testlabel2'] is not defined
+ msg: "{{ label_out }}"
diff --git a/roles/lib_openshift/src/test/integration/oc_obj.yml b/roles/lib_openshift/src/test/integration/oc_obj.yml
new file mode 100755
index 000000000..c22a2f6a9
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_obj.yml
@@ -0,0 +1,207 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_obj.yml -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: create test project
+ oc_project:
+ name: test
+ description: all things test
+ node_selector: ""
+
+ # Create Check #
+ - name: create a dc
+ oc_obj:
+ state: present
+ name: mysql
+ namespace: test
+ kind: dc
+ content:
+ path: /tmp/dcout
+ data:
+ apiVersion: v1
+ kind: DeploymentConfig
+ metadata:
+ labels:
+ name: mysql
+ name: mysql
+ spec:
+ replicas: 1
+ selector: {}
+ strategy:
+ resources: {}
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ name: mysql
+ spec:
+ containers:
+ - env:
+ - name: MYSQL_USER
+ value: mysql
+ - name: MYSQL_PASSWORD
+ value: mysql
+ - name: MYSQL_DATABASE
+ value: mysql
+ - name: MYSQL_ROOT_PASSWORD
+ value: mysql
+ image: openshift/mysql-55-centos7:latest
+ imagePullPolicy: Always
+ name: mysql
+ ports:
+ - containerPort: 3306
+ name: tcp-3306
+ protocol: TCP
+ resources: {}
+ securityContext:
+ capabilities: {}
+ privileged: false
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+ securityContext: {}
+ terminationGracePeriodSeconds: 31
+ triggers:
+ - type: ConfigChange
+ - imageChangeParams:
+ automatic: true
+ containerNames:
+ - mysql
+ from:
+ kind: ImageStreamTag
+ name: mysql:latest
+ type: ImageChange
+
+ - name: fetch created dc
+ oc_obj:
+ name: mysql
+ kind: dc
+ state: list
+ namespace: test
+ register: dcout
+
+ - debug: var=dcout
+
+ - assert:
+ that:
+ - dcout.results.returncode == 0
+ - dcout.results.results[0].metadata.name == 'mysql'
+ # End Create Check #
+
+
+ # Delete Check #
+ - name: delete created dc
+ oc_obj:
+ name: mysql
+ kind: dc
+ state: absent
+ namespace: test
+ register: dcout
+
+ - name: fetch delete dc
+ oc_obj:
+ name: mysql
+ kind: dc
+ state: list
+ namespace: test
+ register: dcout
+
+ - debug: var=dcout
+
+ - assert:
+ that:
+ - dcout.results.returncode == 0
+ - "'\"mysql\" not found' in dcout.results.stderr"
+ # End Delete Check #
+
+ # Delete selector Check #
+ - name: create a dc
+ oc_obj:
+ state: present
+ name: mysql
+ namespace: test
+ kind: dc
+ content:
+ path: /tmp/dcout
+ data:
+ apiVersion: v1
+ kind: DeploymentConfig
+ metadata:
+ labels:
+ name: mysql
+ name: mysql
+ spec:
+ replicas: 1
+ selector: {}
+ strategy:
+ resources: {}
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ name: mysql
+ spec:
+ containers:
+ - env:
+ - name: MYSQL_USER
+ value: mysql
+ - name: MYSQL_PASSWORD
+ value: mysql
+ - name: MYSQL_DATABASE
+ value: mysql
+ - name: MYSQL_ROOT_PASSWORD
+ value: mysql
+ image: openshift/mysql-55-centos7:latest
+ imagePullPolicy: Always
+ name: mysql
+ ports:
+ - containerPort: 3306
+ name: tcp-3306
+ protocol: TCP
+ resources: {}
+ securityContext:
+ capabilities: {}
+ privileged: false
+ terminationMessagePath: /dev/termination-log
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+ securityContext: {}
+ terminationGracePeriodSeconds: 31
+ triggers:
+ - type: ConfigChange
+ - imageChangeParams:
+ automatic: true
+ containerNames:
+ - mysql
+ from:
+ kind: ImageStreamTag
+ name: mysql:latest
+ type: ImageChange
+
+ - name: delete using selector
+ oc_obj:
+ namespace: test
+ selector: name=mysql
+ kind: dc
+ state: absent
+ register: dcout
+
+ - debug: var=dcout
+
+ - name: get the dc
+ oc_obj:
+ namespace: test
+ selector: name=mysql
+ kind: dc
+ state: list
+ register: dcout
+
+ - debug: var=dcout
+
+ - assert:
+ that:
+ - dcout.results.returncode == 0
+ - dcout.results.results[0]["items"]|length == 0
diff --git a/roles/lib_openshift/src/test/integration/oc_process.yml b/roles/lib_openshift/src/test/integration/oc_process.yml
new file mode 100755
index 000000000..7ea4c6b99
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_process.yml
@@ -0,0 +1,83 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/:../../../../lib_utils/library
+
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ vars:
+ template_name: mysql-ephemeral
+ ns_name: test
+
+ post_tasks:
+ - name: get the mysql-ephemeral template
+ oc_obj:
+ name: mysql-ephemeral
+ state: list
+ namespace: openshift
+ kind: template
+ register: mysqltempl
+
+ - name: fix namespace
+ yedit:
+ src: /tmp/mysql-template
+ key: metadata.namespace
+ value: test
+ backup: false
+ content: "{{ mysqltempl.results.results[0] | to_yaml }}"
+
+ - name: create the test namespace
+ oc_obj:
+ name: test
+ state: present
+ namespace: test
+ kind: namespace
+ content:
+ path: /tmp/ns_test
+ data:
+ apiVersion: v1
+ kind: Namespace
+ metadata:
+ name: test
+ spec:
+ finalizers:
+ - openshift.io/origin
+ - kubernetes
+ register: mysqltempl
+
+ - name: create the mysql-ephemeral template
+ oc_obj:
+ name: mysql-ephemeral
+ state: present
+ namespace: test
+ kind: template
+ files:
+ - /tmp/mysql-template
+ delete_after: True
+ register: mysqltempl
+
+ - name: process mysql-ephemeral
+ oc_process:
+ template_name: mysql-ephemeral
+ namespace: test
+ params:
+ NAMESPACE: test
+ DATABASE_SERVICE_NAME: testdb
+ create: False
+ reconcile: false
+ register: procout
+
+ - assert:
+ that:
+ - not procout.changed
+ - procout.results.results['items'][0]['metadata']['name'] == 'testdb'
+ - procout.results.results['items'][0]['kind'] == 'Service'
+ - procout.results.results['items'][1]['metadata']['name'] == 'testdb'
+ - procout.results.results['items'][1]['kind'] == 'DeploymentConfig'
+ msg: process failed on template
+
+ - name: remove namespace test
+ oc_obj:
+ kind: namespace
+ name: test
+ namespace: test
+ state: absent
diff --git a/roles/lib_openshift/src/test/integration/oc_project.yml b/roles/lib_openshift/src/test/integration/oc_project.yml
new file mode 100755
index 000000000..9f700c62c
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_project.yml
@@ -0,0 +1,83 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_project.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: create a project
+ oc_project:
+ display_name: operations project
+ name: operations
+ state: present
+ description: All things operations
+ node_selector:
+ - ops_only=true
+ register: projout
+ - debug: var=projout
+
+ - assert:
+ that:
+ - "projout.results.results['metadata']['name'] == 'operations'"
+ - projout.changed
+ msg: project create failed.
+
+ - name: create a project
+ oc_project:
+ display_name: operations project
+ name: operations
+ state: present
+ description: All things operations
+ node_selector:
+ - ops_only=true
+ register: projout
+ - debug: var=projout
+
+ - assert:
+ that:
+ - "projout.results.results['metadata']['name'] == 'operations'"
+ - projout.changed == False
+ msg: project create failed.
+
+ - name: update a project
+ oc_project:
+ display_name: operations project one
+ name: operations
+ state: present
+ description: All things operations
+ node_selector:
+ - ops_only=true
+ register: projout
+ - debug: var=projout
+
+ - assert:
+ that:
+ - "projout.results.results['metadata']['annotations']['openshift.io/display-name'] == 'operations project one'"
+ - projout.changed == True
+ msg: project create failed.
+
+ - name: update a project
+ oc_project:
+ name: operations
+ state: list
+ register: projout
+ - debug: var=projout
+
+ - assert:
+ that:
+ - "projout.results['metadata']['annotations']['openshift.io/display-name'] == 'operations project one'"
+ - projout.changed == False
+ - projout.state == 'list'
+ msg: project list failed.
+
+ - name: delete a project
+ oc_project:
+ name: operations
+ state: absent
+ register: projout
+ - debug: var=projout
+
+ - assert:
+ that:
+ - projout.changed == True
+ msg: project delete failed.
diff --git a/roles/lib_openshift/src/test/integration/oc_pvc.yml b/roles/lib_openshift/src/test/integration/oc_pvc.yml
new file mode 100755
index 000000000..fb3a4781f
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_pvc.yml
@@ -0,0 +1,28 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_pvc.yml -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: create pvc
+ oc_pvc:
+ state: present
+ name: oc-pvc-create-test
+ namespace: default
+ volume_capacity: 3G
+ access_modes:
+ - ReadWriteOnce
+ selector:
+ foo: bar
+ storage_class_name: my-storage-class-name
+ register: pvcout
+ - debug: var=pvcout
+
+ - assert:
+ that:
+ - pvcout.results.results[0]['metadata']['name'] == 'oc-pvc-create-test'
+ - pvcout.results.results[0]['spec']['storageClassName'] == 'my-storage-class-name'
+ - pvcout.results.results[0]['spec']['selector']['matchLabels']['foo'] == 'bar'
+ - pvcout.changed
+ msg: pvc create failed.
diff --git a/roles/lib_openshift/src/test/integration/oc_route.yml b/roles/lib_openshift/src/test/integration/oc_route.yml
new file mode 100755
index 000000000..b9d635eaa
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_route.yml
@@ -0,0 +1,117 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_route.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: create route
+ oc_route:
+ name: test
+ namespace: default
+ tls_termination: edge
+ cert_content: testing cert
+ cacert_content: testing cacert
+ key_content: key content
+ service_name: test
+ host: test.example
+ register: routeout
+ - debug: var=routeout
+
+ - assert:
+ that: "routeout.results.results[0]['metadata']['name'] == 'test'"
+ msg: route create failed
+
+ - name: get route
+ oc_route:
+ state: list
+ name: test
+ namespace: default
+ register: routeout
+ - debug: var=routeout
+
+ - assert:
+ that: "routeout.results[0]['metadata']['name'] == 'test'"
+ msg: get route failed
+
+ - name: delete route
+ oc_route:
+ state: absent
+ name: test
+ namespace: default
+ register: routeout
+ - debug: var=routeout
+
+ - assert:
+ that: "routeout.results.returncode == 0"
+ msg: delete route failed
+
+ - name: create route
+ oc_route:
+ name: test
+ namespace: default
+ tls_termination: edge
+ cert_content: testing cert
+ cacert_content: testing cacert
+ key_content: testing key
+ service_name: test
+ host: test.example
+ register: routeout
+ - debug: var=routeout
+
+ - name: create route noop
+ oc_route:
+ name: test
+ namespace: default
+ tls_termination: edge
+ cert_content: testing cert
+ cacert_content: testing cacert
+ key_content: testing key
+ service_name: test
+ host: test.example
+ register: routeout
+ - debug: var=routeout
+
+ - assert:
+ that: "routeout.changed == False"
+ msg: Route create not idempotent
+
+ - name: delete route
+ oc_route:
+ name: test
+ namespace: default
+ state: absent
+ register: routeout
+
+ - name: create route
+ oc_route:
+ name: test
+ namespace: default
+ tls_termination: passthrough
+ service_name: test
+ host: test.example
+ port: 8443
+ register: routeout
+
+ - assert:
+ that:
+ - "routeout.changed == True"
+ - "routeout.results['results'][0]['spec']['port']['targetPort'] == 8443"
+ msg: Route create not idempotent
+
+ - name: create route
+ oc_route:
+ name: test
+ namespace: default
+ tls_termination: passthrough
+ service_name: test
+ host: test.example
+ port: 8444
+ register: routeout
+ - debug: var=routeout
+
+ - assert:
+ that:
+ - "routeout.changed == True"
+ - "routeout.results.results[0]['spec']['port']['targetPort'] == 8444"
+ msg: Route update not idempotent
diff --git a/roles/lib_openshift/src/test/integration/oc_scale.yml b/roles/lib_openshift/src/test/integration/oc_scale.yml
new file mode 100755
index 000000000..43a42c589
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_scale.yml
@@ -0,0 +1,111 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_scale.yml -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: list oc scale for default router dc
+ oc_scale:
+ state: list
+ name: router
+ namespace: default
+ kind: dc
+ register: scaleout
+ - debug: var=scaleout
+
+ - assert:
+ that:
+ - "'result' in scaleout"
+ - scaleout.result > 0
+ msg: "Did not find 'result' in returned value or result not > 0."
+
+ - name: get the rc for router
+ oc_obj:
+ state: list
+ kind: dc
+ namespace: default
+ selector: router=router
+ register: rcout
+ - debug:
+ msg: "{{ rcout.results.results[0]['items'][-1]['metadata']['name'] }}"
+
+ - name: scale dc to 1
+ oc_scale:
+ name: router
+ namespace: default
+ kind: dc
+ replicas: 1
+ register: scaleout
+ - debug: var=scaleout
+
+ # The preferred method here would be to let the module
+ # detect when its finished and time out
+ - name: let the scale happen
+ pause:
+ seconds: 10
+ when: scaleout.changed
+
+ - name: fetch the current router pods
+ oc_obj:
+ selector: router=router
+ namespace: default
+ kind: pod
+ state: list
+ register: pods
+ - debug: var=pods
+
+ - assert:
+ that:
+ - "'results' in pods and 'results' in pods.results"
+ - "{{ pods.results.results[0]['items']|length }} == 1"
+ msg: "Did not find 1 replica in scale results."
+
+ - name: scale dc to 2
+ oc_scale:
+ name: router
+ namespace: default
+ kind: dc
+ replicas: 2
+ register: scaleout
+ - debug: var=scaleout
+
+ # The preferred method here would be to let the module
+ # detect when its finished and time out
+ - name: let the scale happen
+ pause:
+ seconds: 30
+
+ - name: fetch the current router pods
+ oc_obj:
+ selector: router=router
+ namespace: default
+ kind: pod
+ state: list
+ register: pods
+ - debug: var=pods
+
+ - assert:
+ that:
+ - "'results' in pods and 'results' in pods.results"
+ - "{{ pods.results.results[0]['items']|length }} == 2"
+ msg: "Did not find 1 replica in scale results."
+
+
+ # Test scale on non-existent dc
+ - name: scale non-existent dc
+ oc_scale:
+ name: not_there
+ kind: dc
+ replicas: 2
+ register: scaleout
+ ignore_errors: True
+
+ - debug: var=scaleout
+
+ - assert:
+ that:
+ - scaleout.changed == False
+ - scaleout.msg.returncode == 1
+ - "'msg' in scaleout and 'stderr' in scaleout.msg"
+ msg: "Deploymentconfig exists. This should error."
diff --git a/roles/lib_openshift/src/test/integration/oc_secret.yml b/roles/lib_openshift/src/test/integration/oc_secret.yml
new file mode 100755
index 000000000..e0456bd6e
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_secret.yml
@@ -0,0 +1,125 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ vars_prompt:
+ - name: cli_master_test
+ prompt: "Master to run against"
+ private: false
+ default: localhost
+
+ vars:
+ secret_name: secret-int-test
+ ns_name: default
+ config_path: "/tmp/{{ secret_name }}--config.yml"
+ passwords_path: "/tmp/{{ secret_name }}--passwords.yml"
+
+ post_tasks:
+
+ - name: Setup our files to test with
+ copy:
+ dest: "{{ item.name }}"
+ content: "{{ item.content }}"
+ with_items:
+ - name: "{{ config_path }}"
+ content: |
+ value: True
+ - name: "{{ passwords_path }}"
+ content: |
+ test1
+ test2
+ test3
+ test4
+
+
+ - name: Make sure we're starting with a clean slate
+ oc_secret:
+ state: absent
+ namespace: "{{ ns_name }}"
+ name: "{{ secret_name }}"
+ register: secret_out
+
+ - name: Test adding a secret - Act
+ oc_secret:
+ state: present
+ namespace: "{{ ns_name }}"
+ name: "{{ secret_name }}"
+ files:
+ - name: config.yml
+ path: "{{ config_path }}"
+ - name: passwords.yml
+ path: "{{ passwords_path }}"
+ register: secret_out
+
+ - name: Test adding a secret - Assert
+ assert:
+ that:
+ - "secret_out.results.returncode == 0"
+ - "secret_out.changed == True"
+
+ - name: Test secret present idempotentcy - Act
+ oc_secret:
+ state: present
+ namespace: "{{ ns_name }}"
+ name: "{{ secret_name }}"
+ files:
+ - name: config.yml
+ path: "{{ config_path }}"
+ - name: passwords.yml
+ path: "{{ passwords_path }}"
+ register: secret_out
+
+ - name: Test secret present idempotentcy - Assert
+ assert:
+ that:
+ - "secret_out.changed == false"
+
+ - name: Test list secrets - Act
+ oc_secret:
+ state: list
+ namespace: "{{ ns_name }}"
+ name: "{{ secret_name }}"
+ register: secret_out
+
+ - name: Test list secrets - Assert
+ assert:
+ that:
+ - "secret_out.changed == false"
+ - "secret_out.results.exists == true"
+
+
+ - name: Test secret absent - Act
+ oc_secret:
+ state: absent
+ namespace: "{{ ns_name }}"
+ name: "{{ secret_name }}"
+ register: secret_out
+
+ - name: Test secret absent - Assert
+ assert:
+ that:
+ - "secret_out.changed == true"
+ - "secret_out.results.returncode == 0"
+
+ - name: Test secret absent idempotentcy - Act
+ oc_secret:
+ state: absent
+ namespace: "{{ ns_name }}"
+ name: "{{ secret_name }}"
+ register: secret_out
+
+ - name: Test secret idempotentcy - Assert
+ assert:
+ that:
+ - "secret_out.changed == false"
+
+
+ - name: Clean up the files we created
+ file:
+ state: absent
+ path: "{{ item }}"
+ with_items:
+ - "/tmp/{{ secret_name }}--config.yml"
+ - "/tmp/{{ secret_name }}--passwords.yml"
diff --git a/roles/lib_openshift/src/test/integration/oc_service.yml b/roles/lib_openshift/src/test/integration/oc_service.yml
new file mode 100755
index 000000000..29535f24a
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_service.yml
@@ -0,0 +1,133 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_service.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: create the default registry service
+ oc_service:
+ namespace: default
+ name: test-registry
+ ports:
+ - name: 9000-tcp
+ port: 9000
+ protocol: TCP
+ targetPort: 9000
+ selector:
+ test-registtry: default
+ session_affinity: ClientIP
+ service_type: ClusterIP
+ labels:
+ component: test-registry
+ infra: registry
+ register: svc_out
+ - debug: var=svc_out
+
+ - assert:
+ that:
+ - "svc_out.results.results[0]['metadata']['name'] == 'test-registry'"
+ - svc_out.changed
+ - "svc_out.results.results[0]['metadata']['labels']['component'] == 'test-registry'"
+ - "svc_out.results.results[0]['metadata']['labels']['infra'] == 'registry'"
+ msg: service create failed.
+
+ # Test idempotent create
+ - name: NOOP create the default registry service
+ oc_service:
+ namespace: default
+ name: test-registry
+ ports:
+ - name: 9000-tcp
+ port: 9000
+ protocol: TCP
+ targetPort: 9000
+ selector:
+ test-registtry: default
+ session_affinity: ClientIP
+ service_type: ClusterIP
+ register: svc_out
+
+ - assert:
+ that:
+ - "svc_out.results.results[0]['metadata']['name'] == 'test-registry'"
+ - svc_out.changed == False
+ msg: service create failed. No changes expected
+
+ - name: create the default registry service
+ oc_service:
+ namespace: default
+ name: test-registry
+ ports:
+ - name: 9000-tcp
+ port: 9000
+ protocol: TCP
+ targetPort: 9000
+ selector:
+ test-registtry: default
+ session_affinity: ClientIP
+ service_type: ClusterIP
+ register: svc_out
+
+ - assert:
+ that: "svc_out.results.results[0]['metadata']['name'] == 'test-registry'"
+ msg: service create failed
+
+ - name: oc_service
+ oc_service:
+ name: test-registry
+ namespace: default
+ state: list
+ register: svc_out
+
+ - assert:
+ that: "svc_out.results.results[0]['metadata']['name'] == 'test-registry'"
+ msg: service create failed
+
+ - name: create the default registry service
+ oc_service:
+ namespace: default
+ name: test-registry
+ ports:
+ - name: 9001-tcp
+ port: 9001
+ protocol: TCP
+ targetPort: 9001
+ selector:
+ test-registtry: default
+ session_affinity: ClientIP
+ service_type: ClusterIP
+ register: svc_out
+
+ - assert:
+ that: "svc_out.results.results[0]['spec']['ports'][0]['name'] == '9001-tcp'"
+ msg: service update failed
+
+ - name: oc delete service
+ oc_service:
+ name: test-registry
+ namespace: default
+ state: absent
+ register: svc_out
+ - debug: var=svc_out
+
+ - assert:
+ that:
+ - "svc_out.results['returncode'] == 0"
+ - "svc_out.results.results == ''"
+ msg: service delete failed
+
+ - name: oc get service
+ oc_service:
+ name: test-registry
+ namespace: default
+ state: list
+ register: svc_out
+ - debug: var=svc_out
+
+ - assert:
+ that:
+ - svc_out.changed == False
+ - svc_out.results.returncode == 0
+ - "'not found' in svc_out.results.stderr"
+ msg: service get failed
diff --git a/roles/lib_openshift/src/test/integration/oc_serviceaccount.yml b/roles/lib_openshift/src/test/integration/oc_serviceaccount.yml
new file mode 100755
index 000000000..46369b8f4
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_serviceaccount.yml
@@ -0,0 +1,101 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ vars_prompt:
+ - name: cli_master_test
+ prompt: "Master to run against"
+ private: false
+ default: localhost
+
+ vars:
+ service_account_name: serviceaccount-int-test
+ ns_name: default
+
+ post_tasks:
+ - name: Make sure we start clean - Arrange
+ oc_serviceaccount:
+ state: absent
+ name: "{{ service_account_name }}"
+ namespace: "{{ ns_name }}"
+
+ - name: List when account does not exist - Act
+ oc_serviceaccount:
+ state: list
+ name: "{{ service_account_name }}"
+ namespace: "{{ ns_name }}"
+ register: saout
+
+ - name: List when account does not exist - Assert
+ assert:
+ that:
+ - "saout.changed == False"
+ - "saout.state == 'list'"
+ - "saout.results == [{}]"
+
+ - name: create serviceaccount - Act
+ oc_serviceaccount:
+ name: "{{ service_account_name }}"
+ namespace: "{{ ns_name }}"
+ secrets:
+ - one
+ - two
+ - three
+ register: saout
+
+ - name: create serviceaccount - Assert
+ assert:
+ that:
+ - "saout.changed == True"
+ - "saout.state == 'present'"
+ - "saout.results.returncode == 0"
+ - "saout.results.results.0.metadata.name == '{{ service_account_name }}'"
+ - "saout.results.results.0.metadata.namespace == '{{ ns_name }}'"
+
+ - name: create serviceaccount - check idempotency - Act
+ oc_serviceaccount:
+ name: "{{ service_account_name }}"
+ namespace: "{{ ns_name }}"
+ secrets:
+ - one
+ - two
+ - three
+ register: saout
+
+ - name: create serviceaccount - check idempotency - Assert
+ assert:
+ that:
+ - "saout.changed == False"
+ - "saout.state == 'present'"
+ - "saout.results.returncode == 0"
+ - "saout.results.results.0.metadata.name == '{{ service_account_name }}'"
+ - "saout.results.results.0.metadata.namespace == '{{ ns_name }}'"
+
+ - name: Delete serviceaccount - Act
+ oc_serviceaccount:
+ state: absent
+ name: "{{ service_account_name }}"
+ namespace: "{{ ns_name }}"
+ register: saout
+
+ - name: Delete serviceaccount - Assert
+ assert:
+ that:
+ - "saout.changed == True"
+ - "saout.state == 'absent'"
+ - "saout.results.returncode == 0"
+
+ - name: Delete serviceaccount - check idempotency - Act
+ oc_serviceaccount:
+ state: absent
+ name: "{{ service_account_name }}"
+ namespace: "{{ ns_name }}"
+ register: saout
+
+ - name: Delete serviceaccount - check idempotency - Assert
+ assert:
+ that:
+ - "saout.changed == False"
+ - "saout.state == 'absent'"
diff --git a/roles/lib_openshift/src/test/integration/oc_serviceaccount_secret.yml b/roles/lib_openshift/src/test/integration/oc_serviceaccount_secret.yml
new file mode 100755
index 000000000..d3bd9f3aa
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_serviceaccount_secret.yml
@@ -0,0 +1,79 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+
+ vars:
+ namespace: default
+ service_account_name: someserviceaccountname
+ secret_name: somesecretname
+
+ vars_prompt:
+ - name: cli_master_test
+ prompt: "Master to run against"
+ private: false
+ default: localhost
+
+ post_tasks:
+ - name: create service account to test with - Arrange
+ oc_serviceaccount:
+ namespace: "{{ namespace }}"
+ name: "{{ service_account_name }}"
+
+ - name: create secret to test with - Arrange
+ oc_secret:
+ namespace: "{{ namespace }}"
+ name: "{{ secret_name }}"
+ contents:
+ - path: blah
+ data: blahdeblah
+
+ - name: Ensure the service account and secret are not linked - Arrange
+ oc_serviceaccount_secret:
+ state: absent
+ service_account: "{{ service_account_name }}"
+ secret: "{{ secret_name }}"
+ namespace: "{{ namespace }}"
+
+ - name: get secrets of a service account - Act
+ oc_serviceaccount_secret:
+ state: list
+ service_account: builder
+ namespace: "{{ namespace }}"
+ register: sasecretout
+
+ - name: get secrets of a service account - Assert
+ assert:
+ that:
+ - "sasecretout.changed == False"
+ - "sasecretout.state == 'list'"
+ - "sasecretout.results | length > 0"
+
+ - name: Test linking a service account and secret - Act
+ oc_serviceaccount_secret:
+ service_account: "{{ service_account_name }}"
+ secret: "{{ secret_name }}"
+ namespace: "{{ namespace }}"
+ register: sasecretout
+
+ - name: Test linking a service account and secret - Assert
+ assert:
+ that:
+ - "sasecretout.changed == True"
+ - "sasecretout.state == 'present'"
+ - "sasecretout.results.returncode == 0"
+ - "sasecretout.results.results | length > 0"
+
+ - name: Test linking a service account and secret - idempotency - Act
+ oc_serviceaccount_secret:
+ service_account: "{{ service_account_name }}"
+ secret: "{{ secret_name }}"
+ namespace: "{{ namespace }}"
+ register: sasecretout
+
+ - name: Test linking a service account and secret - idempotency - Assert
+ assert:
+ that:
+ - "sasecretout.changed == False"
+ - "sasecretout.state == 'present'"
diff --git a/roles/lib_openshift/src/test/integration/oc_storageclass.yml b/roles/lib_openshift/src/test/integration/oc_storageclass.yml
new file mode 100755
index 000000000..c82f9dedb
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_storageclass.yml
@@ -0,0 +1,87 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_storageclass.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: create a storageclass
+ oc_storageclass:
+ name: testsc
+ parameters:
+ type: gp2
+ default_storage_class: "true"
+ register: sc_out
+ - debug: var=sc_out
+
+ - assert:
+ that:
+ - "sc_out.results.results[0]['metadata']['name'] == 'testsc'"
+ - sc_out.changed
+ - "sc_out.results.results[0]['parameters']['type'] == 'gp2'"
+ msg: storageclass create failed.
+
+ # Test idempotent create
+ - name: NOOP create the storageclass
+ oc_storageclass:
+ name: testsc
+ parameters:
+ type: gp2
+ default_storage_class: "true"
+ register: sc_out
+
+ - assert:
+ that:
+ - "sc_out.results.results[0]['metadata']['name'] == 'testsc'"
+ - sc_out.changed == False
+ msg: storageclass create failed. No changes expected
+
+ - name: test list storageclass
+ oc_storageclass:
+ name: testsc
+ state: list
+ register: sc_out
+ - debug: var=sc_out
+
+ - assert:
+ that: "sc_out.results[0]['metadata']['name'] == 'testsc'"
+ msg: storageclass list failed
+
+ - name: update the storageclass
+ oc_storageclass:
+ name: testsc
+ parameters:
+ type: gp2
+ encrypted: "true"
+ default_storage_class: "true"
+ register: sc_out
+
+ - assert:
+ that: "sc_out.results.results[0]['parameters']['encrypted'] == 'true'"
+ msg: storageclass update failed
+
+ - name: oc delete storageclass
+ oc_storageclass:
+ name: testsc
+ state: absent
+ register: sc_out
+ - debug: var=sc_out
+
+ - assert:
+ that:
+ - "sc_out.results['returncode'] == 0"
+ - "sc_out.results.results == {}"
+ msg: storageclass delete failed
+
+ - name: oc get storageclass
+ oc_storageclass:
+ name: testsc
+ state: list
+ register: sc_out
+ - debug: var=sc_out
+
+ - assert:
+ that:
+ - sc_out.changed == False
+ - "sc_out.results == [{}]"
+ msg: storageclass get failed
diff --git a/roles/lib_openshift/src/test/integration/oc_user.yml b/roles/lib_openshift/src/test/integration/oc_user.yml
new file mode 100755
index 000000000..9b4290052
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_user.yml
@@ -0,0 +1,240 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+#
+# ./oc_user.yml -e "cli_master_test=$OPENSHIFT_MASTER
+#
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+
+ vars:
+ test_user: testuser@email.com
+ test_user_fullname: "Test User"
+ pre_tasks:
+ - name: ensure needed vars are defined
+ fail:
+ msg: "{{ item }} no defined"
+ when: item is not defined
+ with_items:
+ - cli_master_test # ansible inventory instance to run playbook against
+
+ tasks:
+ - name: delete test user (so future tests work)
+ oc_user:
+ state: absent
+ username: "{{ test_user }}"
+
+ - name: get user list
+ oc_user:
+ state: list
+ username: "{{ test_user }}"
+ register: user_out
+ - name: "assert test user does not exist"
+ assert:
+ that: user_out['results'][0] == {}
+ msg: "{{ user_out }}"
+
+ - name: get all list
+ oc_user:
+ state: list
+ register: user_out
+ #- debug: var=user_out
+
+ - name: add test user
+ oc_user:
+ state: present
+ username: "{{ test_user }}"
+ full_name: "{{ test_user_fullname }}"
+ register: user_out
+ - name: assert result set to changed
+ assert:
+ that: user_out['changed'] == True
+ msg: "{{ user_out }}"
+
+ - name: check test user actually added
+ oc_user:
+ state: list
+ username: "{{ test_user }}"
+ register: user_out
+ - name: assert user actually added
+ assert:
+ that: user_out['results'][0]['metadata']['name'] == "{{ test_user }}" and
+ user_out['results'][0]['fullName'] == "{{ test_user_fullname }}"
+ msg: "{{ user_out }}"
+
+ - name: re-add test user
+ oc_user:
+ state: present
+ username: "{{ test_user }}"
+ full_name: "{{ test_user_fullname }}"
+ register: user_out
+ - name: assert re-add result set to not changed
+ assert:
+ that: user_out['changed'] == False
+ msg: "{{ user_out }}"
+
+ - name: modify existing user
+ oc_user:
+ state: present
+ username: "{{ test_user }}"
+ full_name: 'Something Different'
+ register: user_out
+ - name: assert modify existing user result set to changed
+ assert:
+ that: user_out['changed'] == True
+ msg: "{{ user_out }}"
+
+ - name: check modify test user
+ oc_user:
+ state: list
+ username: "{{ test_user }}"
+ register: user_out
+ - name: assert modification successful
+ assert:
+ that: user_out['results'][0]['metadata']['name'] == "{{ test_user }}" and
+ user_out['results'][0]['fullName'] == 'Something Different'
+ msg: "{{ user_out }}"
+
+ - name: delete test user
+ oc_user:
+ state: absent
+ username: "{{ test_user }}"
+ register: user_out
+ - name: assert delete marked changed
+ assert:
+ that: user_out['changed'] == True
+ msg: "{{ user_out }}"
+
+ - name: check delete user
+ oc_user:
+ state: list
+ username: "{{ test_user }}"
+ register: user_out
+ - name: assert deletion successful
+ assert:
+ that: user_out['results'][0] == {}
+ msg: "{{ user_out }}"
+
+ - name: re-delete test user
+ oc_user:
+ state: absent
+ username: "{{ test_user }}"
+ register: user_out
+ - name: check re-delete marked not changed
+ assert:
+ that: user_out['changed'] == False
+ msg: "{{ user_out }}"
+
+ - name: delete test group
+ oc_obj:
+ kind: group
+ state: absent
+ name: integration-test-group
+
+ - name: create test group
+ command: oadm groups new integration-test-group
+
+ - name: check group creation
+ oc_obj:
+ kind: group
+ state: list
+ name: integration-test-group
+ register: user_out
+ - name: assert test group created
+ assert:
+ that: user_out['results']['results'][0]['metadata']['name'] == "integration-test-group"
+ msg: "{{ user_out }}"
+
+ - name: create user with group membership
+ oc_user:
+ state: present
+ username: "{{ test_user }}"
+ groups:
+ - "integration-test-group"
+ register: user_out
+ - debug: var=user_out
+ - name: get group user members
+ oc_obj:
+ kind: group
+ state: list
+ name: integration-test-group
+ register: user_out
+ - name: assert user group membership
+ assert:
+ that: "'{{ test_user }}' in user_out['results']['results'][0]['users'][0]"
+ msg: "{{ user_out }}"
+
+ - name: delete second test group
+ oc_obj:
+ kind: group
+ state: absent
+ name: integration-test-group2
+
+ - name: create empty second group
+ command: oadm groups new integration-test-group2
+
+ - name: update user with second group membership
+ oc_user:
+ state: present
+ username: "{{ test_user }}"
+ groups:
+ - "integration-test-group"
+ - "integration-test-group2"
+ register: user_out
+ - name: assert adding more group changed
+ assert:
+ that: user_out['changed'] == True
+
+ - name: get group memberships
+ oc_obj:
+ kind: group
+ state: list
+ name: "{{ item }}"
+ with_items:
+ - integration-test-group
+ - integration-test-group2
+ register: user_out
+ - name: assert user member of above groups
+ assert:
+ that: "'{{ test_user }}' in user_out['results'][0]['results']['results'][0]['users'] and \
+ '{{ test_user }}' in user_out['results'][1]['results']['results'][0]['users']"
+ msg: "{{ user_out }}"
+
+ - name: update user with only one group
+ oc_user:
+ state: present
+ username: "{{ test_user }}"
+ groups:
+ - "integration-test-group2"
+ register: user_out
+ - assert:
+ that: user_out['changed'] == True
+
+ - name: get group memberships
+ oc_obj:
+ kind: group
+ state: list
+ name: "{{ item }}"
+ with_items:
+ - "integration-test-group"
+ - "integration-test-group2"
+ register: user_out
+ - debug: var=user_out
+ - name: assert proper user membership
+ assert:
+ that: "'{{ test_user }}' not in user_out['results'][0]['results']['results'][0]['users'] and \
+ '{{ test_user }}' in user_out['results'][1]['results']['results'][0]['users']"
+
+ - name: clean up test groups
+ oc_obj:
+ kind: group
+ state: absent
+ name: "{{ item }}"
+ with_items:
+ - "integration-test-group"
+ - "integration-test-group2"
+
+ - name: clean up test user
+ oc_user:
+ state: absent
+ username: "{{ test_user }}"
diff --git a/roles/lib_openshift/src/test/integration/oc_version.yml b/roles/lib_openshift/src/test/integration/oc_version.yml
new file mode 100755
index 000000000..52336d8da
--- /dev/null
+++ b/roles/lib_openshift/src/test/integration/oc_version.yml
@@ -0,0 +1,17 @@
+#!/usr/bin/ansible-playbook --module-path=../../../library/
+# ./oc_version.yml -e "cli_master_test=$OPENSHIFT_MASTER
+---
+- hosts: "{{ cli_master_test }}"
+ gather_facts: no
+ user: root
+ tasks:
+ - name: Get openshift version
+ oc_version:
+ register: versionout
+
+ - debug: var=versionout
+
+ - assert:
+ that:
+ - "'oc_numeric' in versionout.results.keys()"
+ msg: "Did not find 'oc_numeric' in version results."
diff --git a/roles/lib_openshift/src/test/unit/test_oc_adm_manage_node.py b/roles/lib_openshift/src/test/unit/test_oc_adm_manage_node.py
new file mode 100755
index 000000000..312b1ecbb
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_adm_manage_node.py
@@ -0,0 +1,277 @@
+'''
+ Unit tests for oc_adm_manage_node
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_adm_manage_node import ManageNode, locate_oc_binary # noqa: E402
+
+
+class ManageNodeTest(unittest.TestCase):
+ '''
+ Test class for oc_adm_manage_node
+ '''
+
+ @mock.patch('oc_adm_manage_node.Utils.create_tmpfile_copy')
+ @mock.patch('oc_adm_manage_node.ManageNode.openshift_cmd')
+ def test_list_pods(self, mock_openshift_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'node': ['ip-172-31-49-140.ec2.internal'],
+ 'schedulable': None,
+ 'selector': None,
+ 'pod_selector': None,
+ 'list_pods': True,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'evacuate': False,
+ 'grace_period': False,
+ 'dry_run': False,
+ 'force': False}
+
+ pod_list = '''{
+ "metadata": {},
+ "items": [
+ {
+ "metadata": {
+ "name": "docker-registry-1-xuhik",
+ "generateName": "docker-registry-1-",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/pods/docker-registry-1-xuhik",
+ "uid": "ae2a25a2-e316-11e6-80eb-0ecdc51fcfc4",
+ "resourceVersion": "1501",
+ "creationTimestamp": "2017-01-25T15:55:23Z",
+ "labels": {
+ "deployment": "docker-registry-1",
+ "deploymentconfig": "docker-registry",
+ "docker-registry": "default"
+ },
+ "annotations": {
+ "openshift.io/deployment-config.latest-version": "1",
+ "openshift.io/deployment-config.name": "docker-registry",
+ "openshift.io/deployment.name": "docker-registry-1",
+ "openshift.io/scc": "restricted"
+ }
+ },
+ "spec": {}
+ },
+ {
+ "metadata": {
+ "name": "router-1-kp3m3",
+ "generateName": "router-1-",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/pods/router-1-kp3m3",
+ "uid": "9e71f4a5-e316-11e6-80eb-0ecdc51fcfc4",
+ "resourceVersion": "1456",
+ "creationTimestamp": "2017-01-25T15:54:56Z",
+ "labels": {
+ "deployment": "router-1",
+ "deploymentconfig": "router",
+ "router": "router"
+ },
+ "annotations": {
+ "openshift.io/deployment-config.latest-version": "1",
+ "openshift.io/deployment-config.name": "router",
+ "openshift.io/deployment.name": "router-1",
+ "openshift.io/scc": "hostnetwork"
+ }
+ },
+ "spec": {}
+ }]
+}'''
+
+ mock_openshift_cmd.side_effect = [
+ {"cmd": "/usr/bin/oadm manage-node ip-172-31-49-140.ec2.internal --list-pods",
+ "results": pod_list,
+ "returncode": 0}
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = ManageNode.run_ansible(params, False)
+
+ # returned a single node
+ self.assertTrue(len(results['results']['nodes']) == 1)
+ # returned 2 pods
+ self.assertTrue(len(results['results']['nodes']['ip-172-31-49-140.ec2.internal']) == 2)
+
+ @mock.patch('oc_adm_manage_node.Utils.create_tmpfile_copy')
+ @mock.patch('oc_adm_manage_node.ManageNode.openshift_cmd')
+ def test_schedulable_false(self, mock_openshift_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'node': ['ip-172-31-49-140.ec2.internal'],
+ 'schedulable': False,
+ 'selector': None,
+ 'pod_selector': None,
+ 'list_pods': False,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'evacuate': False,
+ 'grace_period': False,
+ 'dry_run': False,
+ 'force': False}
+
+ node = [{
+ "apiVersion": "v1",
+ "kind": "Node",
+ "metadata": {
+ "creationTimestamp": "2017-01-26T14:34:43Z",
+ "labels": {
+ "beta.kubernetes.io/arch": "amd64",
+ "beta.kubernetes.io/instance-type": "m4.large",
+ "beta.kubernetes.io/os": "linux",
+ "failure-domain.beta.kubernetes.io/region": "us-east-1",
+ "failure-domain.beta.kubernetes.io/zone": "us-east-1c",
+ "hostname": "opstest-node-compute-0daaf",
+ "kubernetes.io/hostname": "ip-172-31-51-111.ec2.internal",
+ "ops_node": "old",
+ "region": "us-east-1",
+ "type": "compute"
+ },
+ "name": "ip-172-31-51-111.ec2.internal",
+ "resourceVersion": "6936",
+ "selfLink": "/api/v1/nodes/ip-172-31-51-111.ec2.internal",
+ "uid": "93d7fdfb-e3d4-11e6-a982-0e84250fc302"
+ },
+ "spec": {
+ "externalID": "i-06bb330e55c699b0f",
+ "providerID": "aws:///us-east-1c/i-06bb330e55c699b0f",
+ }}]
+
+ mock_openshift_cmd.side_effect = [
+ {"cmd": "/usr/bin/oc get node -o json ip-172-31-49-140.ec2.internal",
+ "results": node,
+ "returncode": 0},
+ {"cmd": "/usr/bin/oadm manage-node ip-172-31-49-140.ec2.internal --schedulable=False",
+ "results": "NAME STATUS AGE\n" +
+ "ip-172-31-49-140.ec2.internal Ready,SchedulingDisabled 5h\n",
+ "returncode": 0}]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = ManageNode.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['nodes'][0]['name'], 'ip-172-31-49-140.ec2.internal')
+ self.assertEqual(results['results']['nodes'][0]['schedulable'], False)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py b/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py
new file mode 100755
index 000000000..77787fe87
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_adm_registry.py
@@ -0,0 +1,370 @@
+#!/usr/bin/env python
+'''
+ Unit tests for oc adm registry
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_adm_registry import Registry, locate_oc_binary # noqa: E402
+
+
+# pylint: disable=too-many-public-methods
+class RegistryTest(unittest.TestCase):
+ '''
+ Test class for Registry
+ '''
+ dry_run = '''{
+ "kind": "List",
+ "apiVersion": "v1",
+ "metadata": {},
+ "items": [
+ {
+ "kind": "ServiceAccount",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "registry",
+ "creationTimestamp": null
+ }
+ },
+ {
+ "kind": "ClusterRoleBinding",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "registry-registry-role",
+ "creationTimestamp": null
+ },
+ "userNames": [
+ "system:serviceaccount:default:registry"
+ ],
+ "groupNames": null,
+ "subjects": [
+ {
+ "kind": "ServiceAccount",
+ "namespace": "default",
+ "name": "registry"
+ }
+ ],
+ "roleRef": {
+ "kind": "ClusterRole",
+ "name": "system:registry"
+ }
+ },
+ {
+ "kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "docker-registry",
+ "creationTimestamp": null,
+ "labels": {
+ "docker-registry": "default"
+ }
+ },
+ "spec": {
+ "strategy": {
+ "resources": {}
+ },
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ }
+ ],
+ "replicas": 1,
+ "test": false,
+ "selector": {
+ "docker-registry": "default"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "docker-registry": "default"
+ }
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "registry-storage",
+ "emptyDir": {}
+ }
+ ],
+ "containers": [
+ {
+ "name": "registry",
+ "image": "openshift3/ose-docker-registry:v3.5.0.39",
+ "ports": [
+ {
+ "containerPort": 5000
+ }
+ ],
+ "env": [
+ {
+ "name": "REGISTRY_HTTP_ADDR",
+ "value": ":5000"
+ },
+ {
+ "name": "REGISTRY_HTTP_NET",
+ "value": "tcp"
+ },
+ {
+ "name": "REGISTRY_HTTP_SECRET",
+ "value": "WQjSGeUu5KFZRTwGeIXgwIjyraNDLmdJblsFbtzZdF8="
+ },
+ {
+ "name": "REGISTRY_MIDDLEWARE_REPOSITORY_OPENSHIFT_ENFORCEQUOTA",
+ "value": "false"
+ }
+ ],
+ "resources": {
+ "requests": {
+ "cpu": "100m",
+ "memory": "256Mi"
+ }
+ },
+ "volumeMounts": [
+ {
+ "name": "registry-storage",
+ "mountPath": "/registry"
+ }
+ ],
+ "livenessProbe": {
+ "httpGet": {
+ "path": "/healthz",
+ "port": 5000
+ },
+ "initialDelaySeconds": 10,
+ "timeoutSeconds": 5
+ },
+ "readinessProbe": {
+ "httpGet": {
+ "path": "/healthz",
+ "port": 5000
+ },
+ "timeoutSeconds": 5
+ },
+ "securityContext": {
+ "privileged": false
+ }
+ }
+ ],
+ "nodeSelector": {
+ "type": "infra"
+ },
+ "serviceAccountName": "registry",
+ "serviceAccount": "registry"
+ }
+ }
+ },
+ "status": {
+ "latestVersion": 0,
+ "observedGeneration": 0,
+ "replicas": 0,
+ "updatedReplicas": 0,
+ "availableReplicas": 0,
+ "unavailableReplicas": 0
+ }
+ },
+ {
+ "kind": "Service",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "docker-registry",
+ "creationTimestamp": null,
+ "labels": {
+ "docker-registry": "default"
+ }
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "5000-tcp",
+ "port": 5000,
+ "targetPort": 5000
+ }
+ ],
+ "selector": {
+ "docker-registry": "default"
+ },
+ "clusterIP": "172.30.119.110",
+ "sessionAffinity": "ClientIP"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ }
+ ]}'''
+
+ @mock.patch('oc_adm_registry.locate_oc_binary')
+ @mock.patch('oc_adm_registry.Utils._write')
+ @mock.patch('oc_adm_registry.Utils.create_tmpfile_copy')
+ @mock.patch('oc_adm_registry.Registry._run')
+ def test_state_present(self, mock_cmd, mock_tmpfile_copy, mock_write, mock_oc_binary):
+ ''' Testing state present '''
+ params = {'state': 'present',
+ 'debug': False,
+ 'namespace': 'default',
+ 'name': 'docker-registry',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'images': None,
+ 'latest_images': None,
+ 'labels': {"docker-registry": "default", "another-label": "val"},
+ 'ports': ['5000'],
+ 'replicas': 1,
+ 'selector': 'type=infra',
+ 'service_account': 'registry',
+ 'mount_host': None,
+ 'volume_mounts': None,
+ 'env_vars': {},
+ 'enforce_quota': False,
+ 'force': False,
+ 'daemonset': False,
+ 'tls_key': None,
+ 'tls_certificate': None,
+ 'edits': []}
+
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server (NotFound): deploymentconfigs "docker-registry" not found'),
+ (1, '', 'Error from server (NotFound): service "docker-registry" not found'),
+ (0, RegistryTest.dry_run, ''),
+ (0, '', ''),
+ (0, '', ''),
+ ]
+
+ mock_tmpfile_copy.return_value = '/tmp/mocked_kubeconfig'
+
+ mock_oc_binary.return_value = 'oc'
+
+ results = Registry.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ for result in results['results']['results']:
+ self.assertEqual(result['returncode'], 0)
+
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'dc', 'docker-registry', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'svc', 'docker-registry', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'adm', 'registry',
+ "--labels=another-label=val,docker-registry=default",
+ '--ports=5000', '--replicas=1', '--selector=type=infra',
+ '--service-account=registry', '--dry-run=True', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None), ])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_adm_router.py b/roles/lib_openshift/src/test/unit/test_oc_adm_router.py
new file mode 100755
index 000000000..dcf768e08
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_adm_router.py
@@ -0,0 +1,480 @@
+#!/usr/bin/env python
+'''
+ Unit tests for oc adm router
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_adm_router import Router, locate_oc_binary # noqa: E402
+
+
+# pylint: disable=too-many-public-methods
+class RouterTest(unittest.TestCase):
+ '''
+ Test class for Router
+ '''
+ dry_run = '''{
+ "kind": "List",
+ "apiVersion": "v1",
+ "metadata": {},
+ "items": [
+ {
+ "kind": "ServiceAccount",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "creationTimestamp": null
+ }
+ },
+ {
+ "kind": "ClusterRoleBinding",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router-router-role",
+ "creationTimestamp": null
+ },
+ "userNames": [
+ "system:serviceaccount:default:router"
+ ],
+ "groupNames": null,
+ "subjects": [
+ {
+ "kind": "ServiceAccount",
+ "namespace": "default",
+ "name": "router"
+ }
+ ],
+ "roleRef": {
+ "kind": "ClusterRole",
+ "name": "system:router"
+ }
+ },
+ {
+ "kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "strategy": {
+ "type": "Rolling",
+ "rollingParams": {
+ "maxUnavailable": "25%",
+ "maxSurge": 0
+ },
+ "resources": {}
+ },
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ }
+ ],
+ "replicas": 2,
+ "test": false,
+ "selector": {
+ "router": "router"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "server-certificate",
+ "secret": {
+ "secretName": "router-certs"
+ }
+ }
+ ],
+ "containers": [
+ {
+ "name": "router",
+ "image": "openshift3/ose-haproxy-router:v3.5.0.39",
+ "ports": [
+ {
+ "containerPort": 80
+ },
+ {
+ "containerPort": 443
+ },
+ {
+ "name": "stats",
+ "containerPort": 1936,
+ "protocol": "TCP"
+ }
+ ],
+ "env": [
+ {
+ "name": "DEFAULT_CERTIFICATE_DIR",
+ "value": "/etc/pki/tls/private"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HOSTNAME"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTPS_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTP_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_INSECURE",
+ "value": "false"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_INTERNAL_ADDRESS"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_PARTITION_PATH"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_PASSWORD"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_PRIVKEY",
+ "value": "/etc/secret-volume/router.pem"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_USERNAME"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_VXLAN_GW_CIDR"
+ },
+ {
+ "name": "ROUTER_SERVICE_HTTPS_PORT",
+ "value": "443"
+ },
+ {
+ "name": "ROUTER_SERVICE_HTTP_PORT",
+ "value": "80"
+ },
+ {
+ "name": "ROUTER_SERVICE_NAME",
+ "value": "router"
+ },
+ {
+ "name": "ROUTER_SERVICE_NAMESPACE",
+ "value": "default"
+ },
+ {
+ "name": "ROUTER_SUBDOMAIN"
+ },
+ {
+ "name": "STATS_PASSWORD",
+ "value": "eSfUICQyyr"
+ },
+ {
+ "name": "STATS_PORT",
+ "value": "1936"
+ },
+ {
+ "name": "STATS_USERNAME",
+ "value": "admin"
+ }
+ ],
+ "resources": {
+ "requests": {
+ "cpu": "100m",
+ "memory": "256Mi"
+ }
+ },
+ "volumeMounts": [
+ {
+ "name": "server-certificate",
+ "readOnly": true,
+ "mountPath": "/etc/pki/tls/private"
+ }
+ ],
+ "livenessProbe": {
+ "httpGet": {
+ "path": "/healthz",
+ "port": 1936,
+ "host": "localhost"
+ },
+ "initialDelaySeconds": 10
+ },
+ "readinessProbe": {
+ "httpGet": {
+ "path": "/healthz",
+ "port": 1936,
+ "host": "localhost"
+ },
+ "initialDelaySeconds": 10
+ },
+ "imagePullPolicy": "IfNotPresent"
+ }
+ ],
+ "nodeSelector": {
+ "type": "infra"
+ },
+ "serviceAccountName": "router",
+ "serviceAccount": "router",
+ "hostNetwork": true,
+ "securityContext": {}
+ }
+ }
+ },
+ "status": {
+ "latestVersion": 0,
+ "observedGeneration": 0,
+ "replicas": 0,
+ "updatedReplicas": 0,
+ "availableReplicas": 0,
+ "unavailableReplicas": 0
+ }
+ },
+ {
+ "kind": "Service",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ },
+ "annotations": {
+ "service.alpha.openshift.io/serving-cert-secret-name": "router-certs"
+ }
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "80-tcp",
+ "port": 80,
+ "targetPort": 80
+ },
+ {
+ "name": "443-tcp",
+ "port": 443,
+ "targetPort": 443
+ },
+ {
+ "name": "1936-tcp",
+ "protocol": "TCP",
+ "port": 1936,
+ "targetPort": 1936
+ }
+ ],
+ "selector": {
+ "router": "router"
+ }
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ }
+ ]
+}'''
+
+ @mock.patch('oc_adm_router.locate_oc_binary')
+ @mock.patch('oc_adm_router.Utils._write')
+ @mock.patch('oc_adm_router.Utils.create_tmpfile_copy')
+ @mock.patch('oc_adm_router.Router._run')
+ def test_state_present(self, mock_cmd, mock_tmpfile_copy, mock_write, mock_oc_binary):
+ ''' Testing a create '''
+ params = {'state': 'present',
+ 'debug': False,
+ 'namespace': 'default',
+ 'name': 'router',
+ 'default_cert': None,
+ 'cert_file': None,
+ 'key_file': None,
+ 'cacert_file': None,
+ 'labels': {"router": "router", "another-label": "val"},
+ 'ports': ['80:80', '443:443'],
+ 'images': None,
+ 'latest_images': None,
+ 'clusterip': None,
+ 'portalip': None,
+ 'session_affinity': None,
+ 'service_type': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'replicas': 2,
+ 'selector': 'type=infra',
+ 'service_account': 'router',
+ 'router_type': None,
+ 'host_network': None,
+ 'external_host': None,
+ 'external_host_vserver': None,
+ 'external_host_insecure': False,
+ 'external_host_partition_path': None,
+ 'external_host_username': None,
+ 'external_host_password': None,
+ 'external_host_private_key': None,
+ 'expose_metrics': False,
+ 'metrics_image': None,
+ 'stats_user': None,
+ 'stats_password': None,
+ 'stats_port': 1936,
+ 'edits': []}
+
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server (NotFound): deploymentconfigs "router" not found'),
+ (1, '', 'Error from server (NotFound): service "router" not found'),
+ (1, '', 'Error from server (NotFound): serviceaccount "router" not found'),
+ (1, '', 'Error from server (NotFound): secret "router-certs" not found'),
+ (1, '', 'Error from server (NotFound): clsuterrolebinding "router-router-role" not found'),
+ (0, RouterTest.dry_run, ''),
+ (0, '', ''),
+ (0, '', ''),
+ (0, '', ''),
+ (0, '', ''),
+ (0, '', ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc',
+ ]
+
+ results = Router.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ for result in results['results']['results']:
+ self.assertEqual(result['returncode'], 0)
+
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'dc', 'router', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'svc', 'router', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'sa', 'router', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'secret', 'router-certs', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'clusterrolebinding', 'router-router-role', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'adm', 'router', 'router', '--expose-metrics=False', '--external-host-insecure=False',
+ "--labels=another-label=val,router=router",
+ '--ports=80:80,443:443', '--replicas=2', '--selector=type=infra', '--service-account=router',
+ '--stats-port=1936', '--dry-run=True', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None)])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_clusterrole.py b/roles/lib_openshift/src/test/unit/test_oc_clusterrole.py
new file mode 100755
index 000000000..189f16bda
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_clusterrole.py
@@ -0,0 +1,115 @@
+'''
+ Unit tests for oc clusterrole
+'''
+
+import copy
+import os
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_clusterrole import OCClusterRole # noqa: E402
+
+
+class OCClusterRoleTest(unittest.TestCase):
+ '''
+ Test class for OCClusterRole
+ '''
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'present',
+ 'name': 'operations',
+ 'rules': [
+ {'apiGroups': [''],
+ 'attributeRestrictions': None,
+ 'verbs': ['create', 'delete', 'deletecollection',
+ 'get', 'list', 'patch', 'update', 'watch'],
+ 'resources': ['persistentvolumes']}
+ ],
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ }
+
+ @mock.patch('oc_clusterrole.locate_oc_binary')
+ @mock.patch('oc_clusterrole.Utils.create_tmpfile_copy')
+ @mock.patch('oc_clusterrole.Utils._write')
+ @mock.patch('oc_clusterrole.OCClusterRole._run')
+ def test_adding_a_clusterrole(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_loc_binary):
+ ''' Testing adding a project '''
+
+ params = copy.deepcopy(OCClusterRoleTest.params)
+
+ clusterrole = '''{
+ "apiVersion": "v1",
+ "kind": "ClusterRole",
+ "metadata": {
+ "creationTimestamp": "2017-03-27T14:19:09Z",
+ "name": "operations",
+ "resourceVersion": "23",
+ "selfLink": "/oapi/v1/clusterrolesoperations",
+ "uid": "57d358fe-12f8-11e7-874a-0ec502977670"
+ },
+ "rules": [
+ {
+ "apiGroups": [
+ ""
+ ],
+ "attributeRestrictions": null,
+ "resources": [
+ "persistentvolumes"
+ ],
+ "verbs": [
+ "create",
+ "delete",
+ "deletecollection",
+ "get",
+ "list",
+ "patch",
+ "update",
+ "watch"
+ ]
+ }
+ ]
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: clusterrole "operations" not found'),
+ (1, '', 'Error from server: namespaces "operations" not found'),
+ (0, '', ''), # created
+ (0, clusterrole, ''), # fetch it
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_loc_binary.side_effect = [
+ 'oc',
+ ]
+
+ # Act
+ results = OCClusterRole.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['results']['results']['metadata']['name'], 'operations')
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'clusterrole', 'operations', '-o', 'json'], None),
+ mock.call(['oc', 'get', 'clusterrole', 'operations', '-o', 'json'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY], None),
+ mock.call(['oc', 'get', 'clusterrole', 'operations', '-o', 'json'], None),
+ ])
diff --git a/roles/lib_openshift/src/test/unit/test_oc_configmap.py b/roles/lib_openshift/src/test/unit/test_oc_configmap.py
new file mode 100755
index 000000000..27042c64b
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_configmap.py
@@ -0,0 +1,239 @@
+'''
+ Unit tests for oc configmap
+'''
+
+import copy
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_configmap import OCConfigMap, locate_oc_binary # noqa: E402
+
+
+class OCConfigMapTest(unittest.TestCase):
+ '''
+ Test class for OCConfigMap
+ '''
+ params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'present',
+ 'debug': False,
+ 'name': 'configmap',
+ 'from_file': {},
+ 'from_literal': {},
+ 'namespace': 'test'}
+
+ @mock.patch('oc_configmap.Utils._write')
+ @mock.patch('oc_configmap.Utils.create_tmpfile_copy')
+ @mock.patch('oc_configmap.OCConfigMap._run')
+ def test_create_configmap(self, mock_run, mock_tmpfile_copy, mock_write):
+ ''' Testing a configmap create '''
+ # TODO
+ return
+ params = copy.deepcopy(OCConfigMapTest.params)
+ params['from_file'] = {'test': '/root/file'}
+ params['from_literal'] = {'foo': 'bar'}
+
+ configmap = '''{
+ "apiVersion": "v1",
+ "data": {
+ "foo": "bar",
+ "test": "this is a file\\n"
+ },
+ "kind": "ConfigMap",
+ "metadata": {
+ "creationTimestamp": "2017-03-20T20:24:35Z",
+ "name": "configmap",
+ "namespace": "test"
+ }
+ }'''
+
+ mock_run.side_effect = [
+ (1, '', 'Error from server (NotFound): configmaps "configmap" not found'),
+ (0, '', ''),
+ (0, configmap, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCConfigMap.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'configmap')
+
+ @mock.patch('oc_configmap.Utils._write')
+ @mock.patch('oc_configmap.Utils.create_tmpfile_copy')
+ @mock.patch('oc_configmap.OCConfigMap._run')
+ def test_update_configmap(self, mock_run, mock_tmpfile_copy, mock_write):
+ ''' Testing a configmap create '''
+ params = copy.deepcopy(OCConfigMapTest.params)
+ params['from_file'] = {'test': '/root/file'}
+ params['from_literal'] = {'foo': 'bar', 'deployment_type': 'openshift-enterprise'}
+
+ configmap = '''{
+ "apiVersion": "v1",
+ "data": {
+ "foo": "bar",
+ "test": "this is a file\\n"
+ },
+ "kind": "ConfigMap",
+ "metadata": {
+ "creationTimestamp": "2017-03-20T20:24:35Z",
+ "name": "configmap",
+ "namespace": "test"
+
+ }
+ }'''
+
+ mod_configmap = '''{
+ "apiVersion": "v1",
+ "data": {
+ "foo": "bar",
+ "deployment_type": "openshift-enterprise",
+ "test": "this is a file\\n"
+ },
+ "kind": "ConfigMap",
+ "metadata": {
+ "creationTimestamp": "2017-03-20T20:24:35Z",
+ "name": "configmap",
+ "namespace": "test"
+
+ }
+ }'''
+
+ mock_run.side_effect = [
+ (0, configmap, ''),
+ (0, mod_configmap, ''),
+ (0, configmap, ''),
+ (0, '', ''),
+ (0, mod_configmap, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCConfigMap.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'configmap')
+ self.assertEqual(results['results']['results'][0]['data']['deployment_type'], 'openshift-enterprise')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_env.py b/roles/lib_openshift/src/test/unit/test_oc_env.py
new file mode 100755
index 000000000..2f416c05e
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_env.py
@@ -0,0 +1,548 @@
+'''
+ Unit tests for oc_env
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_env import OCEnv, locate_oc_binary # noqa: E402
+
+
+class OCEnvTest(unittest.TestCase):
+ '''
+ Test class for OCEnv
+ '''
+
+ @mock.patch('oc_env.locate_oc_binary')
+ @mock.patch('oc_env.Utils.create_tmpfile_copy')
+ @mock.patch('oc_env.OCEnv._run')
+ def test_listing_all_env_vars(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing listing all environment variables from a dc'''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'list',
+ 'namespace': 'default',
+ 'name': 'router',
+ 'kind': 'dc',
+ 'env_vars': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ }
+
+ dc_results = '''{
+ "apiVersion": "v1",
+ "kind": "DeploymentConfig",
+ "metadata": {
+ "creationTimestamp": "2017-02-02T15:58:49Z",
+ "generation": 8,
+ "labels": {
+ "router": "router"
+ },
+ "name": "router",
+ "namespace": "default",
+ "resourceVersion": "513678"
+ },
+ "spec": {
+ "replicas": 2,
+ "selector": {
+ "router": "router"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "env": [
+ {
+ "name": "DEFAULT_CERTIFICATE_DIR",
+ "value": "/etc/pki/tls/private"
+ },
+ {
+ "name": "DEFAULT_CERTIFICATE_PATH",
+ "value": "/etc/pki/tls/private/tls.crt"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HOSTNAME"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTPS_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTP_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_INSECURE",
+ "value": "false"
+ }
+ ],
+ "name": "router"
+ }
+ ]
+ }
+ },
+ "test": false,
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ }
+ ]
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, dc_results, ''), # First call to the mock
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock_adminkubeconfig',
+ ]
+
+ # Act
+ results = OCEnv.run_ansible(params, False)
+
+ # Assert
+ self.assertFalse(results['changed'])
+ for env_var in results['results']:
+ if env_var == {'name': 'DEFAULT_CERTIFICATE_DIR', 'value': '/etc/pki/tls/private'}:
+ break
+ else:
+ self.fail('Did not find environment variables in results.')
+ self.assertEqual(results['state'], 'list')
+
+ # Making sure our mocks were called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'dc', 'router', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @mock.patch('oc_env.locate_oc_binary')
+ @mock.patch('oc_env.Utils.create_tmpfile_copy')
+ @mock.patch('oc_env.OCEnv._run')
+ def test_adding_env_vars(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Test add environment variables to a dc'''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'present',
+ 'namespace': 'default',
+ 'name': 'router',
+ 'kind': 'dc',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ 'env_vars': {'SOMEKEY': 'SOMEVALUE'},
+ }
+
+ dc_results = '''{
+ "apiVersion": "v1",
+ "kind": "DeploymentConfig",
+ "metadata": {
+ "creationTimestamp": "2017-02-02T15:58:49Z",
+ "generation": 8,
+ "labels": {
+ "router": "router"
+ },
+ "name": "router",
+ "namespace": "default",
+ "resourceVersion": "513678"
+ },
+ "spec": {
+ "replicas": 2,
+ "selector": {
+ "router": "router"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "env": [
+ {
+ "name": "DEFAULT_CERTIFICATE_DIR",
+ "value": "/etc/pki/tls/private"
+ },
+ {
+ "name": "DEFAULT_CERTIFICATE_PATH",
+ "value": "/etc/pki/tls/private/tls.crt"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HOSTNAME"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTPS_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTP_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_INSECURE",
+ "value": "false"
+ }
+ ],
+ "name": "router"
+ }
+ ]
+ }
+ },
+ "test": false,
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ }
+ ]
+ }
+ }'''
+
+ dc_results_after = '''{
+ "apiVersion": "v1",
+ "kind": "DeploymentConfig",
+ "metadata": {
+ "creationTimestamp": "2017-02-02T15:58:49Z",
+ "generation": 8,
+ "labels": {
+ "router": "router"
+ },
+ "name": "router",
+ "namespace": "default",
+ "resourceVersion": "513678"
+ },
+ "spec": {
+ "replicas": 2,
+ "selector": {
+ "router": "router"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "env": [
+ {
+ "name": "DEFAULT_CERTIFICATE_DIR",
+ "value": "/etc/pki/tls/private"
+ },
+ {
+ "name": "DEFAULT_CERTIFICATE_PATH",
+ "value": "/etc/pki/tls/private/tls.crt"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HOSTNAME"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTPS_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTP_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_INSECURE",
+ "value": "false"
+ },
+ {
+ "name": "SOMEKEY",
+ "value": "SOMEVALUE"
+ }
+ ],
+ "name": "router"
+ }
+ ]
+ }
+ },
+ "test": false,
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ }
+ ]
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, dc_results, ''),
+ (0, dc_results, ''),
+ (0, dc_results_after, ''),
+ (0, dc_results_after, ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock_adminkubeconfig',
+ ]
+
+ # Act
+ results = OCEnv.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ for env_var in results['results']:
+ if env_var == {'name': 'SOMEKEY', 'value': 'SOMEVALUE'}:
+ break
+ else:
+ self.fail('Did not find environment variables in results.')
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mocks were called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'dc', 'router', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @mock.patch('oc_env.locate_oc_binary')
+ @mock.patch('oc_env.Utils.create_tmpfile_copy')
+ @mock.patch('oc_env.OCEnv._run')
+ def test_removing_env_vars(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Test add environment variables to a dc'''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'absent',
+ 'namespace': 'default',
+ 'name': 'router',
+ 'kind': 'dc',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ 'env_vars': {'SOMEKEY': 'SOMEVALUE'},
+ }
+
+ dc_results_before = '''{
+ "apiVersion": "v1",
+ "kind": "DeploymentConfig",
+ "metadata": {
+ "creationTimestamp": "2017-02-02T15:58:49Z",
+ "generation": 8,
+ "labels": {
+ "router": "router"
+ },
+ "name": "router",
+ "namespace": "default",
+ "resourceVersion": "513678"
+ },
+ "spec": {
+ "replicas": 2,
+ "selector": {
+ "router": "router"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "env": [
+ {
+ "name": "DEFAULT_CERTIFICATE_DIR",
+ "value": "/etc/pki/tls/private"
+ },
+ {
+ "name": "DEFAULT_CERTIFICATE_PATH",
+ "value": "/etc/pki/tls/private/tls.crt"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HOSTNAME"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTPS_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_HTTP_VSERVER"
+ },
+ {
+ "name": "ROUTER_EXTERNAL_HOST_INSECURE",
+ "value": "false"
+ },
+ {
+ "name": "SOMEKEY",
+ "value": "SOMEVALUE"
+ }
+ ],
+ "name": "router"
+ }
+ ]
+ }
+ },
+ "test": false,
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ }
+ ]
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, dc_results_before, ''),
+ (0, dc_results_before, ''),
+ (0, '', ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock_adminkubeconfig',
+ ]
+
+ # Act
+ results = OCEnv.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['state'], 'absent')
+
+ # Making sure our mocks were called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'dc', 'router', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_group.py b/roles/lib_openshift/src/test/unit/test_oc_group.py
new file mode 100755
index 000000000..8eef37810
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_group.py
@@ -0,0 +1,253 @@
+'''
+ Unit tests for oc group
+'''
+
+import copy
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_group import OCGroup, locate_oc_binary # noqa: E402
+
+
+class OCGroupTest(unittest.TestCase):
+ '''
+ Test class for OCGroup
+ '''
+ params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'present',
+ 'debug': False,
+ 'name': 'acme',
+ 'namespace': 'test'}
+
+ @mock.patch('oc_group.Utils.create_tmpfile_copy')
+ @mock.patch('oc_group.OCGroup._run')
+ def test_create_group(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a group create '''
+ params = copy.deepcopy(OCGroupTest.params)
+
+ group = '''{
+ "kind": "Group",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "acme"
+ },
+ "users": []
+ }'''
+
+ mock_run.side_effect = [
+ (1, '', 'Error from server: groups "acme" not found'),
+ (1, '', 'Error from server: groups "acme" not found'),
+ (0, '', ''),
+ (0, group, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCGroup.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'acme')
+
+ @mock.patch('oc_group.Utils.create_tmpfile_copy')
+ @mock.patch('oc_group.OCGroup._run')
+ def test_failed_get_group(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a group create '''
+ params = copy.deepcopy(OCGroupTest.params)
+ params['state'] = 'list'
+ params['name'] = 'noexist'
+
+ mock_run.side_effect = [
+ (1, '', 'Error from server: groups "acme" not found'),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCGroup.run_ansible(params, False)
+
+ self.assertTrue(results['failed'])
+
+ @mock.patch('oc_group.Utils.create_tmpfile_copy')
+ @mock.patch('oc_group.OCGroup._run')
+ def test_delete_group(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a group create '''
+ params = copy.deepcopy(OCGroupTest.params)
+ params['state'] = 'absent'
+
+ group = '''{
+ "kind": "Group",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "acme"
+ },
+ "users": [
+ "user1"
+ ]
+ }'''
+
+ mock_run.side_effect = [
+ (0, group, ''),
+ (0, '', ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCGroup.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+
+ @mock.patch('oc_group.Utils.create_tmpfile_copy')
+ @mock.patch('oc_group.OCGroup._run')
+ def test_get_group(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a group create '''
+ params = copy.deepcopy(OCGroupTest.params)
+ params['state'] = 'list'
+
+ group = '''{
+ "kind": "Group",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "acme"
+ },
+ "users": [
+ "user1"
+ ]
+ }'''
+
+ mock_run.side_effect = [
+ (0, group, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCGroup.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['results'][0]['metadata']['name'], 'acme')
+ self.assertEqual(results['results'][0]['users'][0], 'user1')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_image.py b/roles/lib_openshift/src/test/unit/test_oc_image.py
new file mode 100755
index 000000000..943c8ca17
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_image.py
@@ -0,0 +1,280 @@
+'''
+ Unit tests for oc image
+'''
+import os
+import sys
+import unittest
+import mock
+import six
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_image import OCImage, locate_oc_binary # noqa: E402
+
+
+class OCImageTest(unittest.TestCase):
+ '''
+ Test class for OCImage
+ '''
+
+ @mock.patch('oc_image.Utils.create_tmpfile_copy')
+ @mock.patch('oc_image.OCImage._run')
+ def test_state_list(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a label list '''
+ params = {'registry_url': 'registry.ops.openshift.com',
+ 'image_name': 'oso-rhel7-zagg-web',
+ 'image_tag': 'int',
+ 'namespace': 'default',
+ 'state': 'list',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ istream = '''{
+ "kind": "ImageStream",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "oso-rhel7-zagg-web",
+ "namespace": "default",
+ "selfLink": "/oapi/v1/namespaces/default/imagestreams/oso-rhel7-zagg-web",
+ "uid": "6ca2b199-dcdb-11e6-8ffd-0a5f8e3e32be",
+ "resourceVersion": "8135944",
+ "generation": 1,
+ "creationTimestamp": "2017-01-17T17:36:05Z",
+ "annotations": {
+ "openshift.io/image.dockerRepositoryCheck": "2017-01-17T17:36:05Z"
+ }
+ },
+ "spec": {
+ "tags": [
+ {
+ "name": "int",
+ "annotations": null,
+ "from": {
+ "kind": "DockerImage",
+ "name": "registry.ops.openshift.com/ops/oso-rhel7-zagg-web:int"
+ },
+ "generation": 1,
+ "importPolicy": {}
+ }
+ ]
+ },
+ "status": {
+ "dockerImageRepository": "172.30.183.164:5000/default/oso-rhel7-zagg-web",
+ "tags": [
+ {
+ "tag": "int",
+ "items": [
+ {
+ "created": "2017-01-17T17:36:05Z",
+ "dockerImageReference": "registry.ops.openshift.com/ops/oso-rhel7-zagg-web@sha256:645bab780cf18a9b764d64b02ca65c39d13cb16f19badd0a49a1668629759392",
+ "image": "sha256:645bab780cf18a9b764d64b02ca65c39d13cb16f19badd0a49a1668629759392",
+ "generation": 1
+ }
+ ]
+ }
+ ]
+ }
+ }
+ '''
+
+ mock_cmd.side_effect = [
+ (0, istream, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCImage.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEquals(results['results']['results'][0]['metadata']['name'], 'oso-rhel7-zagg-web')
+
+ @mock.patch('oc_image.Utils.create_tmpfile_copy')
+ @mock.patch('oc_image.OCImage._run')
+ def test_state_present(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a image present '''
+ params = {'registry_url': 'registry.ops.openshift.com',
+ 'image_name': 'oso-rhel7-zagg-web',
+ 'image_tag': 'int',
+ 'namespace': 'default',
+ 'state': 'present',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ istream = '''{
+ "kind": "ImageStream",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "oso-rhel7-zagg-web",
+ "namespace": "default",
+ "selfLink": "/oapi/v1/namespaces/default/imagestreams/oso-rhel7-zagg-web",
+ "uid": "6ca2b199-dcdb-11e6-8ffd-0a5f8e3e32be",
+ "resourceVersion": "8135944",
+ "generation": 1,
+ "creationTimestamp": "2017-01-17T17:36:05Z",
+ "annotations": {
+ "openshift.io/image.dockerRepositoryCheck": "2017-01-17T17:36:05Z"
+ }
+ },
+ "spec": {
+ "tags": [
+ {
+ "name": "int",
+ "annotations": null,
+ "from": {
+ "kind": "DockerImage",
+ "name": "registry.ops.openshift.com/ops/oso-rhel7-zagg-web:int"
+ },
+ "generation": 1,
+ "importPolicy": {}
+ }
+ ]
+ },
+ "status": {
+ "dockerImageRepository": "172.30.183.164:5000/default/oso-rhel7-zagg-web",
+ "tags": [
+ {
+ "tag": "int",
+ "items": [
+ {
+ "created": "2017-01-17T17:36:05Z",
+ "dockerImageReference": "registry.ops.openshift.com/ops/oso-rhel7-zagg-web@sha256:645bab780cf18a9b764d64b02ca65c39d13cb16f19badd0a49a1668629759392",
+ "image": "sha256:645bab780cf18a9b764d64b02ca65c39d13cb16f19badd0a49a1668629759392",
+ "generation": 1
+ }
+ ]
+ }
+ ]
+ }
+ }
+ '''
+
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: imagestreams "oso-rhel7-zagg-web" not found'),
+ (0, '', ''),
+ (0, istream, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCImage.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['results'][0]['metadata']['name'] == 'oso-rhel7-zagg-web')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_label.py b/roles/lib_openshift/src/test/unit/test_oc_label.py
new file mode 100755
index 000000000..5453266c1
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_label.py
@@ -0,0 +1,285 @@
+'''
+ Unit tests for oc label
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_label import OCLabel, locate_oc_binary # noqa: E402
+
+
+class OCLabelTest(unittest.TestCase):
+ '''
+ Test class for OCLabel
+ '''
+
+ @mock.patch('oc_label.Utils.create_tmpfile_copy')
+ @mock.patch('oc_label.OCLabel._run')
+ def test_state_list(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a label list '''
+ params = {'name': 'default',
+ 'namespace': 'default',
+ 'labels': None,
+ 'state': 'list',
+ 'kind': 'namespace',
+ 'selector': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ ns = '''{
+ "kind": "Namespace",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "default",
+ "selfLink": "/api/v1/namespaces/default",
+ "uid": "c45b9547-e3d3-11e6-ba9c-0eece8f2ce22",
+ "resourceVersion": "403024",
+ "creationTimestamp": "2017-01-26T14:28:55Z",
+ "labels": {
+ "storage_pv_quota": "False"
+ },
+ "annotations": {
+ "openshift.io/node-selector": "",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c1,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000000000/10000",
+ "openshift.io/sa.scc.uid-range": "1000000000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ mock_cmd.side_effect = [
+ (0, ns, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCLabel.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertTrue(results['results']['labels'] == [{'storage_pv_quota': 'False'}])
+
+ @mock.patch('oc_label.Utils.create_tmpfile_copy')
+ @mock.patch('oc_label.OCLabel._run')
+ def test_state_present(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a label list '''
+ params = {'name': 'default',
+ 'namespace': 'default',
+ 'labels': [
+ {'key': 'awesomens', 'value': 'testinglabel'},
+ {'key': 'storage_pv_quota', 'value': 'False'}
+ ],
+ 'state': 'present',
+ 'kind': 'namespace',
+ 'selector': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ ns = '''{
+ "kind": "Namespace",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "default",
+ "selfLink": "/api/v1/namespaces/default",
+ "uid": "c45b9547-e3d3-11e6-ba9c-0eece8f2ce22",
+ "resourceVersion": "403024",
+ "creationTimestamp": "2017-01-26T14:28:55Z",
+ "labels": {
+ "storage_pv_quota": "False"
+ },
+ "annotations": {
+ "openshift.io/node-selector": "",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c1,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000000000/10000",
+ "openshift.io/sa.scc.uid-range": "1000000000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ ns1 = '''{
+ "kind": "Namespace",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "default",
+ "selfLink": "/api/v1/namespaces/default",
+ "uid": "c45b9547-e3d3-11e6-ba9c-0eece8f2ce22",
+ "resourceVersion": "403024",
+ "creationTimestamp": "2017-01-26T14:28:55Z",
+ "labels": {
+ "storage_pv_quota": "False",
+ "awesomens": "testinglabel"
+ },
+ "annotations": {
+ "openshift.io/node-selector": "",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c1,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000000000/10000",
+ "openshift.io/sa.scc.uid-range": "1000000000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ mock_cmd.side_effect = [
+ (0, ns, ''),
+ (0, '', ''),
+ (0, ns1, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCLabel.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['results']['labels'][0] ==
+ {'storage_pv_quota': 'False', 'awesomens': 'testinglabel'})
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_objectvalidator.py b/roles/lib_openshift/src/test/unit/test_oc_objectvalidator.py
new file mode 100755
index 000000000..b19a5a880
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_objectvalidator.py
@@ -0,0 +1,923 @@
+'''
+ Unit tests for oc_objectvalidator
+'''
+
+import os
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_objectvalidator import OCObjectValidator # noqa: E402
+
+
+class OCObjectValidatorTest(unittest.TestCase):
+ '''
+ Test class for OCObjectValidator
+ '''
+
+ maxDiff = None
+
+ @mock.patch('oc_objectvalidator.locate_oc_binary')
+ @mock.patch('oc_objectvalidator.Utils.create_tmpfile_copy')
+ @mock.patch('oc_objectvalidator.OCObjectValidator._run')
+ def test_no_data(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing when both all objects are empty '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ }
+
+ empty = '''{
+ "apiVersion": "v1",
+ "items": [],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+}'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (0, empty, ''),
+
+ # Second call to mock
+ (0, empty, ''),
+
+ # Third call to mock
+ (0, empty, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc',
+ ]
+
+ # Act
+ results = OCObjectValidator.run_ansible(params)
+
+ # Assert
+ self.assertNotIn('failed', results)
+ self.assertEqual(results['msg'], 'All objects are valid.')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'hostsubnet', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'netnamespace', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'namespace', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @mock.patch('oc_objectvalidator.locate_oc_binary')
+ @mock.patch('oc_objectvalidator.Utils.create_tmpfile_copy')
+ @mock.patch('oc_objectvalidator.OCObjectValidator._run')
+ def test_error_code(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing when we fail to get objects '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ }
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (1, '', 'Error.'),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ error_results = {
+ 'returncode': 1,
+ 'stderr': 'Error.',
+ 'stdout': '',
+ 'cmd': 'oc get hostsubnet -o json -n default',
+ 'results': [{}]
+ }
+
+ # Act
+ results = OCObjectValidator.run_ansible(params)
+
+ # Assert
+ self.assertTrue(results['failed'])
+ self.assertEqual(results['msg'], 'Failed to GET hostsubnet.')
+ self.assertEqual(results['state'], 'list')
+ self.assertEqual(results['results'], error_results)
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'hostsubnet', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @mock.patch('oc_objectvalidator.locate_oc_binary')
+ @mock.patch('oc_objectvalidator.Utils.create_tmpfile_copy')
+ @mock.patch('oc_objectvalidator.OCObjectValidator._run')
+ def test_valid_both(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing when both all objects are valid '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ }
+
+ valid_hostsubnet = '''{
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "host": "bar0",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:09Z",
+ "name": "bar0",
+ "namespace": "",
+ "resourceVersion": "986",
+ "selfLink": "/oapi/v1/hostsubnetsbar0",
+ "uid": "528dbb41-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ },
+ {
+ "apiVersion": "v1",
+ "host": "bar1",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:18Z",
+ "name": "bar1",
+ "namespace": "",
+ "resourceVersion": "988",
+ "selfLink": "/oapi/v1/hostsubnetsbar1",
+ "uid": "57710d84-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ },
+ {
+ "apiVersion": "v1",
+ "host": "bar2",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:26Z",
+ "name": "bar2",
+ "namespace": "",
+ "resourceVersion": "991",
+ "selfLink": "/oapi/v1/hostsubnetsbar2",
+ "uid": "5c59a28c-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ }
+ ],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+ }'''
+
+ valid_netnamespace = '''{
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:16Z",
+ "name": "foo0",
+ "namespace": "",
+ "resourceVersion": "959",
+ "selfLink": "/oapi/v1/netnamespacesfoo0",
+ "uid": "0f1c85b2-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "foo0"
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:26Z",
+ "name": "foo1",
+ "namespace": "",
+ "resourceVersion": "962",
+ "selfLink": "/oapi/v1/netnamespacesfoo1",
+ "uid": "14effa0d-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "foo1"
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:36Z",
+ "name": "foo2",
+ "namespace": "",
+ "resourceVersion": "965",
+ "selfLink": "/oapi/v1/netnamespacesfoo2",
+ "uid": "1aabdf84-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "foo2"
+ }
+ ],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+ }'''
+
+ valid_namespace = '''{
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/sa.scc.mcs": "s0:c1,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000000000/10000",
+ "openshift.io/sa.scc.uid-range": "1000000000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:49Z",
+ "name": "default",
+ "namespace": "",
+ "resourceVersion": "165",
+ "selfLink": "/api/v1/namespacesdefault",
+ "uid": "23c0c6aa-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/sa.scc.mcs": "s0:c3,c2",
+ "openshift.io/sa.scc.supplemental-groups": "1000010000/10000",
+ "openshift.io/sa.scc.uid-range": "1000010000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:49Z",
+ "name": "kube-system",
+ "namespace": "",
+ "resourceVersion": "533",
+ "selfLink": "/api/v1/namespaceskube-system",
+ "uid": "23c21758-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/description": "",
+ "openshift.io/display-name": "",
+ "openshift.io/requester": "developer",
+ "openshift.io/sa.scc.mcs": "s0:c9,c4",
+ "openshift.io/sa.scc.supplemental-groups": "1000080000/10000",
+ "openshift.io/sa.scc.uid-range": "1000080000/10000"
+ },
+ "creationTimestamp": "2017-03-02T02:17:16Z",
+ "name": "myproject",
+ "namespace": "",
+ "resourceVersion": "2898",
+ "selfLink": "/api/v1/namespacesmyproject",
+ "uid": "5ae3764d-feee-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "openshift.io/origin",
+ "kubernetes"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/sa.scc.mcs": "s0:c6,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000030000/10000",
+ "openshift.io/sa.scc.uid-range": "1000030000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:51Z",
+ "name": "openshift",
+ "namespace": "",
+ "resourceVersion": "171",
+ "selfLink": "/api/v1/namespacesopenshift",
+ "uid": "24f7b34d-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/sa.scc.mcs": "s0:c5,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000020000/10000",
+ "openshift.io/sa.scc.uid-range": "1000020000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:51Z",
+ "name": "openshift-infra",
+ "namespace": "",
+ "resourceVersion": "169",
+ "selfLink": "/api/v1/namespacesopenshift-infra",
+ "uid": "24a2ed75-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/description": "",
+ "openshift.io/display-name": "",
+ "openshift.io/requester": "developer1",
+ "openshift.io/sa.scc.mcs": "s0:c10,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000090000/10000",
+ "openshift.io/sa.scc.uid-range": "1000090000/10000"
+ },
+ "creationTimestamp": "2017-03-02T02:17:56Z",
+ "name": "yourproject",
+ "namespace": "",
+ "resourceVersion": "2955",
+ "selfLink": "/api/v1/namespacesyourproject",
+ "uid": "72df7fb9-feee-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "openshift.io/origin",
+ "kubernetes"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }
+ ],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+}'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (0, valid_hostsubnet, ''),
+
+ # Second call to mock
+ (0, valid_netnamespace, ''),
+
+ # Third call to mock
+ (0, valid_namespace, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ # Act
+ results = OCObjectValidator.run_ansible(params)
+
+ # Assert
+ self.assertNotIn('failed', results)
+ self.assertEqual(results['msg'], 'All objects are valid.')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'hostsubnet', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'netnamespace', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'namespace', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @mock.patch('oc_objectvalidator.locate_oc_binary')
+ @mock.patch('oc_objectvalidator.Utils.create_tmpfile_copy')
+ @mock.patch('oc_objectvalidator.OCObjectValidator._run')
+ def test_invalid_both(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing when all objects are invalid '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ }
+
+ invalid_hostsubnet = '''{
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "host": "bar0",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:09Z",
+ "name": "bar0",
+ "namespace": "",
+ "resourceVersion": "986",
+ "selfLink": "/oapi/v1/hostsubnetsbar0",
+ "uid": "528dbb41-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ },
+ {
+ "apiVersion": "v1",
+ "host": "bar1",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:18Z",
+ "name": "bar1",
+ "namespace": "",
+ "resourceVersion": "988",
+ "selfLink": "/oapi/v1/hostsubnetsbar1",
+ "uid": "57710d84-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ },
+ {
+ "apiVersion": "v1",
+ "host": "bar2",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:26Z",
+ "name": "bar2",
+ "namespace": "",
+ "resourceVersion": "991",
+ "selfLink": "/oapi/v1/hostsubnetsbar2",
+ "uid": "5c59a28c-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ },
+ {
+ "apiVersion": "v1",
+ "host": "baz1",
+ "hostIP": "1.1.1.1",
+ "kind": "HostSubnet",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:47:49Z",
+ "name": "baz0",
+ "namespace": "",
+ "resourceVersion": "996",
+ "selfLink": "/oapi/v1/hostsubnetsbaz0",
+ "uid": "69f75f87-f478-11e6-aae0-507b9dac97ff"
+ },
+ "subnet": "1.1.0.0/24"
+ }
+ ],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+}'''
+
+ invalid_netnamespace = '''{
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:52Z",
+ "name": "bar0",
+ "namespace": "",
+ "resourceVersion": "969",
+ "selfLink": "/oapi/v1/netnamespacesbar0",
+ "uid": "245d416e-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "bar1"
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:16Z",
+ "name": "foo0",
+ "namespace": "",
+ "resourceVersion": "959",
+ "selfLink": "/oapi/v1/netnamespacesfoo0",
+ "uid": "0f1c85b2-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "foo0"
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:26Z",
+ "name": "foo1",
+ "namespace": "",
+ "resourceVersion": "962",
+ "selfLink": "/oapi/v1/netnamespacesfoo1",
+ "uid": "14effa0d-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "foo1"
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "NetNamespace",
+ "metadata": {
+ "creationTimestamp": "2017-02-16T18:45:36Z",
+ "name": "foo2",
+ "namespace": "",
+ "resourceVersion": "965",
+ "selfLink": "/oapi/v1/netnamespacesfoo2",
+ "uid": "1aabdf84-f478-11e6-aae0-507b9dac97ff"
+ },
+ "netid": 100,
+ "netname": "foo2"
+ }
+ ],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+}'''
+
+ invalid_namespace = '''{
+ "apiVersion": "v1",
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/sa.scc.mcs": "s0:c1,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000000000/10000",
+ "openshift.io/sa.scc.uid-range": "1000000000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:49Z",
+ "name": "default",
+ "namespace": "",
+ "resourceVersion": "165",
+ "selfLink": "/api/v1/namespacesdefault",
+ "uid": "23c0c6aa-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/requester": "",
+ "openshift.io/sa.scc.mcs": "s0:c3,c2",
+ "openshift.io/sa.scc.supplemental-groups": "1000010000/10000",
+ "openshift.io/sa.scc.uid-range": "1000010000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:49Z",
+ "name": "kube-system",
+ "namespace": "",
+ "resourceVersion": "3052",
+ "selfLink": "/api/v1/namespaceskube-system",
+ "uid": "23c21758-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/description": "",
+ "openshift.io/display-name": "",
+ "openshift.io/requester": "developer",
+ "openshift.io/sa.scc.mcs": "s0:c9,c4",
+ "openshift.io/sa.scc.supplemental-groups": "1000080000/10000",
+ "openshift.io/sa.scc.uid-range": "1000080000/10000"
+ },
+ "creationTimestamp": "2017-03-02T02:17:16Z",
+ "name": "myproject",
+ "namespace": "",
+ "resourceVersion": "2898",
+ "selfLink": "/api/v1/namespacesmyproject",
+ "uid": "5ae3764d-feee-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "openshift.io/origin",
+ "kubernetes"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/requester": "",
+ "openshift.io/sa.scc.mcs": "s0:c6,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000030000/10000",
+ "openshift.io/sa.scc.uid-range": "1000030000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:51Z",
+ "name": "openshift",
+ "namespace": "",
+ "resourceVersion": "3057",
+ "selfLink": "/api/v1/namespacesopenshift",
+ "uid": "24f7b34d-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/description": "",
+ "openshift.io/display-name": "",
+ "openshift.io/requester": "system:admin",
+ "openshift.io/sa.scc.mcs": "s0:c10,c5",
+ "openshift.io/sa.scc.supplemental-groups": "1000100000/10000",
+ "openshift.io/sa.scc.uid-range": "1000100000/10000"
+ },
+ "creationTimestamp": "2017-03-02T02:21:15Z",
+ "name": "openshift-fancy",
+ "namespace": "",
+ "resourceVersion": "3072",
+ "selfLink": "/api/v1/namespacesopenshift-fancy",
+ "uid": "e958063c-feee-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "openshift.io/origin",
+ "kubernetes"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/sa.scc.mcs": "s0:c5,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000020000/10000",
+ "openshift.io/sa.scc.uid-range": "1000020000/10000"
+ },
+ "creationTimestamp": "2017-03-02T00:49:51Z",
+ "name": "openshift-infra",
+ "namespace": "",
+ "resourceVersion": "169",
+ "selfLink": "/api/v1/namespacesopenshift-infra",
+ "uid": "24a2ed75-fee2-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "Namespace",
+ "metadata": {
+ "annotations": {
+ "openshift.io/description": "",
+ "openshift.io/display-name": "",
+ "openshift.io/requester": "developer1",
+ "openshift.io/sa.scc.mcs": "s0:c10,c0",
+ "openshift.io/sa.scc.supplemental-groups": "1000090000/10000",
+ "openshift.io/sa.scc.uid-range": "1000090000/10000"
+ },
+ "creationTimestamp": "2017-03-02T02:17:56Z",
+ "name": "yourproject",
+ "namespace": "",
+ "resourceVersion": "2955",
+ "selfLink": "/api/v1/namespacesyourproject",
+ "uid": "72df7fb9-feee-11e6-b45a-507b9dac97ff"
+ },
+ "spec": {
+ "finalizers": [
+ "openshift.io/origin",
+ "kubernetes"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }
+ ],
+ "kind": "List",
+ "metadata": {},
+ "resourceVersion": "",
+ "selfLink": ""
+}'''
+
+ invalid_results = {
+ 'hostsubnets where metadata.name != host': [{
+ 'apiVersion': 'v1',
+ 'host': 'baz1',
+ 'hostIP': '1.1.1.1',
+ 'kind': 'HostSubnet',
+ 'metadata': {
+ 'creationTimestamp': '2017-02-16T18:47:49Z',
+ 'name': 'baz0',
+ 'namespace': '',
+ 'resourceVersion': '996',
+ 'selfLink': '/oapi/v1/hostsubnetsbaz0',
+ 'uid': '69f75f87-f478-11e6-aae0-507b9dac97ff'
+ },
+ 'subnet': '1.1.0.0/24'
+ }],
+ 'netnamespaces where metadata.name != netname': [{
+ 'apiVersion': 'v1',
+ 'kind': 'NetNamespace',
+ 'metadata': {
+ 'creationTimestamp': '2017-02-16T18:45:52Z',
+ 'name': 'bar0',
+ 'namespace': '',
+ 'resourceVersion': '969',
+ 'selfLink': '/oapi/v1/netnamespacesbar0',
+ 'uid': '245d416e-f478-11e6-aae0-507b9dac97ff'
+ },
+ 'netid': 100,
+ 'netname': 'bar1'
+ }],
+ 'namespaces that use reserved names and were not created by infrastructure components': [{
+ 'apiVersion': 'v1',
+ 'kind': 'Namespace',
+ 'metadata': {'annotations': {'openshift.io/requester': '',
+ 'openshift.io/sa.scc.mcs': 's0:c3,c2',
+ 'openshift.io/sa.scc.supplemental-groups': '1000010000/10000',
+ 'openshift.io/sa.scc.uid-range': '1000010000/10000'},
+ 'creationTimestamp': '2017-03-02T00:49:49Z',
+ 'name': 'kube-system',
+ 'namespace': '',
+ 'resourceVersion': '3052',
+ 'selfLink': '/api/v1/namespaceskube-system',
+ 'uid': '23c21758-fee2-11e6-b45a-507b9dac97ff'},
+ 'spec': {'finalizers': ['kubernetes', 'openshift.io/origin']},
+ 'status': {'phase': 'Active'}},
+ {'apiVersion': 'v1',
+ 'kind': 'Namespace',
+ 'metadata': {'annotations': {'openshift.io/requester': '',
+ 'openshift.io/sa.scc.mcs': 's0:c6,c0',
+ 'openshift.io/sa.scc.supplemental-groups': '1000030000/10000',
+ 'openshift.io/sa.scc.uid-range': '1000030000/10000'},
+ 'creationTimestamp': '2017-03-02T00:49:51Z',
+ 'name': 'openshift',
+ 'namespace': '',
+ 'resourceVersion': '3057',
+ 'selfLink': '/api/v1/namespacesopenshift',
+ 'uid': '24f7b34d-fee2-11e6-b45a-507b9dac97ff'},
+ 'spec': {'finalizers': ['kubernetes', 'openshift.io/origin']},
+ 'status': {'phase': 'Active'}},
+ {'apiVersion': 'v1',
+ 'kind': 'Namespace',
+ 'metadata': {'annotations': {'openshift.io/description': '',
+ 'openshift.io/display-name': '',
+ 'openshift.io/requester': 'system:admin',
+ 'openshift.io/sa.scc.mcs': 's0:c10,c5',
+ 'openshift.io/sa.scc.supplemental-groups': '1000100000/10000',
+ 'openshift.io/sa.scc.uid-range': '1000100000/10000'},
+ 'creationTimestamp': '2017-03-02T02:21:15Z',
+ 'name': 'openshift-fancy',
+ 'namespace': '',
+ 'resourceVersion': '3072',
+ 'selfLink': '/api/v1/namespacesopenshift-fancy',
+ 'uid': 'e958063c-feee-11e6-b45a-507b9dac97ff'},
+ 'spec': {'finalizers': ['openshift.io/origin', 'kubernetes']},
+ 'status': {'phase': 'Active'}
+ }],
+ }
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (0, invalid_hostsubnet, ''),
+
+ # Second call to mock
+ (0, invalid_netnamespace, ''),
+
+ # Third call to mock
+ (0, invalid_namespace, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ # Act
+ results = OCObjectValidator.run_ansible(params)
+
+ # Assert
+ self.assertTrue(results['failed'])
+ self.assertIn('All objects are not valid.', results['msg'])
+ self.assertEqual(results['state'], 'list')
+ self.assertEqual(results['results'], invalid_results)
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'hostsubnet', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'netnamespace', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'namespace', '-o', 'json', '-n', 'default'], None),
+ ])
diff --git a/roles/lib_openshift/src/test/unit/test_oc_process.py b/roles/lib_openshift/src/test/unit/test_oc_process.py
new file mode 100755
index 000000000..d887f7636
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_process.py
@@ -0,0 +1,572 @@
+'''
+ Unit tests for oc process
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_process import OCProcess, locate_oc_binary # noqa: E402
+
+
+# pylint: disable=too-many-public-methods
+class OCProcessTest(unittest.TestCase):
+ '''
+ Test class for OCProcess
+ '''
+ mysql = '''{
+ "kind": "Template",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "mysql-ephemeral",
+ "namespace": "openshift",
+ "selfLink": "/oapi/v1/namespaces/openshift/templates/mysql-ephemeral",
+ "uid": "fb8b5f04-e3d3-11e6-a982-0e84250fc302",
+ "resourceVersion": "480",
+ "creationTimestamp": "2017-01-26T14:30:27Z",
+ "annotations": {
+ "iconClass": "icon-mysql-database",
+ "openshift.io/display-name": "MySQL (Ephemeral)",
+ "tags": "database,mysql"
+ }
+ },
+ "objects": [
+ {
+ "apiVersion": "v1",
+ "kind": "Service",
+ "metadata": {
+ "creationTimestamp": null,
+ "name": "${DATABASE_SERVICE_NAME}"
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "mysql",
+ "nodePort": 0,
+ "port": 3306,
+ "protocol": "TCP",
+ "targetPort": 3306
+ }
+ ],
+ "selector": {
+ "name": "${DATABASE_SERVICE_NAME}"
+ },
+ "sessionAffinity": "None",
+ "type": "ClusterIP"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "DeploymentConfig",
+ "metadata": {
+ "creationTimestamp": null,
+ "name": "${DATABASE_SERVICE_NAME}"
+ },
+ "spec": {
+ "replicas": 1,
+ "selector": {
+ "name": "${DATABASE_SERVICE_NAME}"
+ },
+ "strategy": {
+ "type": "Recreate"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "name": "${DATABASE_SERVICE_NAME}"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "capabilities": {},
+ "env": [
+ {
+ "name": "MYSQL_USER",
+ "value": "${MYSQL_USER}"
+ },
+ {
+ "name": "MYSQL_PASSWORD",
+ "value": "${MYSQL_PASSWORD}"
+ },
+ {
+ "name": "MYSQL_DATABASE",
+ "value": "${MYSQL_DATABASE}"
+ }
+ ],
+ "image": " ",
+ "imagePullPolicy": "IfNotPresent",
+ "livenessProbe": {
+ "initialDelaySeconds": 30,
+ "tcpSocket": {
+ "port": 3306
+ },
+ "timeoutSeconds": 1
+ },
+ "name": "mysql",
+ "ports": [
+ {
+ "containerPort": 3306,
+ "protocol": "TCP"
+ }
+ ],
+ "readinessProbe": {
+ "exec": {
+ "command": [
+ "/bin/sh",
+ "-i",
+ "-c",
+ "MYSQL_PWD=$MYSQL_PASSWORD mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'"
+ ]
+ },
+ "initialDelaySeconds": 5,
+ "timeoutSeconds": 1
+ },
+ "resources": {
+ "limits": {
+ "memory": "${MEMORY_LIMIT}"
+ }
+ },
+ "securityContext": {
+ "capabilities": {},
+ "privileged": false
+ },
+ "terminationMessagePath": "/dev/termination-log",
+ "volumeMounts": [
+ {
+ "mountPath": "/var/lib/mysql/data",
+ "name": "${DATABASE_SERVICE_NAME}-data"
+ }
+ ]
+ }
+ ],
+ "dnsPolicy": "ClusterFirst",
+ "restartPolicy": "Always",
+ "volumes": [
+ {
+ "emptyDir": {
+ "medium": ""
+ },
+ "name": "${DATABASE_SERVICE_NAME}-data"
+ }
+ ]
+ }
+ },
+ "triggers": [
+ {
+ "imageChangeParams": {
+ "automatic": true,
+ "containerNames": [
+ "mysql"
+ ],
+ "from": {
+ "kind": "ImageStreamTag",
+ "name": "mysql:${MYSQL_VERSION}",
+ "namespace": "${NAMESPACE}"
+ },
+ "lastTriggeredImage": ""
+ },
+ "type": "ImageChange"
+ },
+ {
+ "type": "ConfigChange"
+ }
+ ]
+ },
+ "status": {}
+ }
+ ],
+ "parameters": [
+ {
+ "name": "MEMORY_LIMIT",
+ "displayName": "Memory Limit",
+ "description": "Maximum amount of memory the container can use.",
+ "value": "512Mi"
+ },
+ {
+ "name": "NAMESPACE",
+ "displayName": "Namespace",
+ "description": "The OpenShift Namespace where the ImageStream resides.",
+ "value": "openshift"
+ },
+ {
+ "name": "DATABASE_SERVICE_NAME",
+ "displayName": "Database Service Name",
+ "description": "The name of the OpenShift Service exposed for the database.",
+ "value": "mysql",
+ "required": true
+ },
+ {
+ "name": "MYSQL_USER",
+ "displayName": "MySQL Connection Username",
+ "description": "Username for MySQL user that will be used for accessing the database.",
+ "generate": "expression",
+ "from": "user[A-Z0-9]{3}",
+ "required": true
+ },
+ {
+ "name": "MYSQL_PASSWORD",
+ "displayName": "MySQL Connection Password",
+ "description": "Password for the MySQL connection user.",
+ "generate": "expression",
+ "from": "[a-zA-Z0-9]{16}",
+ "required": true
+ },
+ {
+ "name": "MYSQL_DATABASE",
+ "displayName": "MySQL Database Name",
+ "description": "Name of the MySQL database accessed.",
+ "value": "sampledb",
+ "required": true
+ },
+ {
+ "name": "MYSQL_VERSION",
+ "displayName": "Version of MySQL Image",
+ "description": "Version of MySQL image to be used (5.5, 5.6 or latest).",
+ "value": "5.6",
+ "required": true
+ }
+ ],
+ "labels": {
+ "template": "mysql-ephemeral-template"
+ }
+}'''
+
+ @mock.patch('oc_process.Utils.create_tmpfile_copy')
+ @mock.patch('oc_process.OCProcess._run')
+ def test_state_list(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'template_name': 'mysql-ephermeral',
+ 'namespace': 'test',
+ 'content': None,
+ 'state': 'list',
+ 'reconcile': False,
+ 'create': False,
+ 'params': {'NAMESPACE': 'test', 'DATABASE_SERVICE_NAME': 'testdb'},
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ mock_cmd.side_effect = [
+ (0, OCProcessTest.mysql, '')
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock_kubeconfig',
+ ]
+
+ results = OCProcess.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'mysql-ephemeral')
+
+ @mock.patch('oc_process.Utils.create_tmpfile_copy')
+ @mock.patch('oc_process.OCProcess._run')
+ def test_process_no_create(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a process with no create '''
+ params = {'template_name': 'mysql-ephermeral',
+ 'namespace': 'test',
+ 'content': None,
+ 'state': 'present',
+ 'reconcile': False,
+ 'create': False,
+ 'params': {'NAMESPACE': 'test', 'DATABASE_SERVICE_NAME': 'testdb'},
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ mysqlproc = '''{
+ "kind": "List",
+ "apiVersion": "v1",
+ "metadata": {},
+ "items": [
+ {
+ "apiVersion": "v1",
+ "kind": "Service",
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "template": "mysql-ephemeral-template"
+ },
+ "name": "testdb"
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "mysql",
+ "nodePort": 0,
+ "port": 3306,
+ "protocol": "TCP",
+ "targetPort": 3306
+ }
+ ],
+ "selector": {
+ "name": "testdb"
+ },
+ "sessionAffinity": "None",
+ "type": "ClusterIP"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ },
+ {
+ "apiVersion": "v1",
+ "kind": "DeploymentConfig",
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "template": "mysql-ephemeral-template"
+ },
+ "name": "testdb"
+ },
+ "spec": {
+ "replicas": 1,
+ "selector": {
+ "name": "testdb"
+ },
+ "strategy": {
+ "type": "Recreate"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "name": "testdb"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "capabilities": {},
+ "env": [
+ {
+ "name": "MYSQL_USER",
+ "value": "userHJJ"
+ },
+ {
+ "name": "MYSQL_PASSWORD",
+ "value": "GITOAduAMaV6k688"
+ },
+ {
+ "name": "MYSQL_DATABASE",
+ "value": "sampledb"
+ }
+ ],
+ "image": " ",
+ "imagePullPolicy": "IfNotPresent",
+ "livenessProbe": {
+ "initialDelaySeconds": 30,
+ "tcpSocket": {
+ "port": 3306
+ },
+ "timeoutSeconds": 1
+ },
+ "name": "mysql",
+ "ports": [
+ {
+ "containerPort": 3306,
+ "protocol": "TCP"
+ }
+ ],
+ "readinessProbe": {
+ "exec": {
+ "command": [
+ "/bin/sh",
+ "-i",
+ "-c",
+ "MYSQL_PWD=$MYSQL_PASSWORD mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'"
+ ]
+ },
+ "initialDelaySeconds": 5,
+ "timeoutSeconds": 1
+ },
+ "resources": {
+ "limits": {
+ "memory": "512Mi"
+ }
+ },
+ "securityContext": {
+ "capabilities": {},
+ "privileged": false
+ },
+ "terminationMessagePath": "/dev/termination-log",
+ "volumeMounts": [
+ {
+ "mountPath": "/var/lib/mysql/data",
+ "name": "testdb-data"
+ }
+ ]
+ }
+ ],
+ "dnsPolicy": "ClusterFirst",
+ "restartPolicy": "Always",
+ "volumes": [
+ {
+ "emptyDir": {
+ "medium": ""
+ },
+ "name": "testdb-data"
+ }
+ ]
+ }
+ },
+ "triggers": [
+ {
+ "imageChangeParams": {
+ "automatic": true,
+ "containerNames": [
+ "mysql"
+ ],
+ "from": {
+ "kind": "ImageStreamTag",
+ "name": "mysql:5.6",
+ "namespace": "test"
+ },
+ "lastTriggeredImage": ""
+ },
+ "type": "ImageChange"
+ },
+ {
+ "type": "ConfigChange"
+ }
+ ]
+ }
+ }
+ ]
+}'''
+
+ mock_cmd.side_effect = [
+ (0, OCProcessTest.mysql, ''),
+ (0, OCProcessTest.mysql, ''),
+ (0, mysqlproc, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock_kubeconfig',
+ ]
+
+ results = OCProcess.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['results']['results']['items'][0]['metadata']['name'], 'testdb')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_project.py b/roles/lib_openshift/src/test/unit/test_oc_project.py
new file mode 100755
index 000000000..fa454d035
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_project.py
@@ -0,0 +1,280 @@
+'''
+ Unit tests for oc project
+'''
+
+import copy
+import os
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_project import OCProject # noqa: E402
+
+
+class OCProjectTest(unittest.TestCase):
+ '''
+ Test class for OCProject
+ '''
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'present',
+ 'display_name': 'operations project',
+ 'name': 'operations',
+ 'node_selector': ['ops_only=True'],
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ 'admin': None,
+ 'admin_role': 'admin',
+ 'description': 'All things operations project',
+ }
+
+ @mock.patch('oc_project.locate_oc_binary')
+ @mock.patch('oc_project.Utils.create_tmpfile_copy')
+ @mock.patch('oc_project.Utils._write')
+ @mock.patch('oc_project.OCProject._run')
+ def test_adding_a_project(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_loc_oc_bin):
+ ''' Testing adding a project '''
+
+ params = copy.deepcopy(OCProjectTest.params)
+
+ # run_ansible input parameters
+ project_results = '''{
+ "kind": "Project",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "operations",
+ "selfLink": "/oapi/v1/projects/operations",
+ "uid": "5e52afb8-ee33-11e6-89f4-0edc441d9666",
+ "resourceVersion": "1584",
+ "labels": {},
+ "annotations": {
+ "openshift.io/node-selector": "ops_only=True",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c3,c2",
+ "openshift.io/sa.scc.supplemental-groups": "1000010000/10000",
+ "openshift.io/sa.scc.uid-range": "1000010000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: namespaces "operations" not found'),
+ (1, '', 'Error from server: namespaces "operations" not found'),
+ (0, '', ''), # created
+ (0, project_results, ''), # fetch it
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_loc_oc_bin.side_effect = [
+ 'oc',
+ ]
+
+ # Act
+ results = OCProject.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['results']['results']['metadata']['name'], 'operations')
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+ mock.call(['oc', 'adm', 'new-project', 'operations', mock.ANY,
+ mock.ANY, mock.ANY, mock.ANY], None),
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+
+ ])
+
+ @mock.patch('oc_project.locate_oc_binary')
+ @mock.patch('oc_project.Utils.create_tmpfile_copy')
+ @mock.patch('oc_project.Utils._write')
+ @mock.patch('oc_project.OCProject._run')
+ def test_modifying_a_project_no_attributes(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_loc_oc_bin):
+ ''' Testing adding a project '''
+ params = copy.deepcopy(self.params)
+ params['display_name'] = None
+ params['node_selector'] = None
+ params['description'] = None
+
+ # run_ansible input parameters
+ project_results = '''{
+ "kind": "Project",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "operations",
+ "selfLink": "/oapi/v1/projects/operations",
+ "uid": "5e52afb8-ee33-11e6-89f4-0edc441d9666",
+ "resourceVersion": "1584",
+ "labels": {},
+ "annotations": {
+ "openshift.io/node-selector": "",
+ "openshift.io/description: "This is a description",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c3,c2",
+ "openshift.io/sa.scc.supplemental-groups": "1000010000/10000",
+ "openshift.io/sa.scc.uid-range": "1000010000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, project_results, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_loc_oc_bin.side_effect = [
+ 'oc',
+ ]
+
+ # Act
+ results = OCProject.run_ansible(params, False)
+
+ # Assert
+ self.assertFalse(results['changed'])
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+ ])
+
+ @mock.patch('oc_project.locate_oc_binary')
+ @mock.patch('oc_project.Utils.create_tmpfile_copy')
+ @mock.patch('oc_project.Utils._write')
+ @mock.patch('oc_project.OCProject._run')
+ def test_modifying_project_attributes(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_loc_oc_bin):
+ ''' Testing adding a project '''
+ params = copy.deepcopy(self.params)
+ params['display_name'] = 'updated display name'
+ params['node_selector'] = 'type=infra'
+ params['description'] = 'updated description'
+
+ # run_ansible input parameters
+ project_results = '''{
+ "kind": "Project",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "operations",
+ "selfLink": "/oapi/v1/projects/operations",
+ "uid": "5e52afb8-ee33-11e6-89f4-0edc441d9666",
+ "resourceVersion": "1584",
+ "labels": {},
+ "annotations": {
+ "openshift.io/node-selector": "",
+ "openshift.io/description": "This is a description",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c3,c2",
+ "openshift.io/sa.scc.supplemental-groups": "1000010000/10000",
+ "openshift.io/sa.scc.uid-range": "1000010000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ mod_project_results = '''{
+ "kind": "Project",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "operations",
+ "selfLink": "/oapi/v1/projects/operations",
+ "uid": "5e52afb8-ee33-11e6-89f4-0edc441d9666",
+ "resourceVersion": "1584",
+ "labels": {},
+ "annotations": {
+ "openshift.io/node-selector": "type=infra",
+ "openshift.io/description": "updated description",
+ "openshift.io/display-name": "updated display name",
+ "openshift.io/sa.initialized-roles": "true",
+ "openshift.io/sa.scc.mcs": "s0:c3,c2",
+ "openshift.io/sa.scc.supplemental-groups": "1000010000/10000",
+ "openshift.io/sa.scc.uid-range": "1000010000/10000"
+ }
+ },
+ "spec": {
+ "finalizers": [
+ "kubernetes",
+ "openshift.io/origin"
+ ]
+ },
+ "status": {
+ "phase": "Active"
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, project_results, ''),
+ (0, project_results, ''),
+ (0, '', ''),
+ (0, mod_project_results, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ mock_loc_oc_bin.side_effect = [
+ 'oc',
+ ]
+
+ # Act
+ results = OCProject.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['results']['results']['metadata']['annotations']['openshift.io/description'], 'updated description')
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+ mock.call(['oc', 'replace', '-f', mock.ANY], None),
+ mock.call(['oc', 'get', 'namespace', 'operations', '-o', 'json'], None),
+ ])
diff --git a/roles/lib_openshift/src/test/unit/test_oc_pvc.py b/roles/lib_openshift/src/test/unit/test_oc_pvc.py
new file mode 100755
index 000000000..a96f2e4a7
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_pvc.py
@@ -0,0 +1,377 @@
+'''
+ Unit tests for oc pvc
+'''
+
+import copy
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_pvc import OCPVC, locate_oc_binary # noqa: E402
+
+
+class OCPVCTest(unittest.TestCase):
+ '''
+ Test class for OCPVC
+ '''
+ params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'present',
+ 'debug': False,
+ 'name': 'mypvc',
+ 'namespace': 'test',
+ 'volume_capacity': '1G',
+ 'selector': {'foo': 'bar', 'abc': 'a123'},
+ 'storage_class_name': 'mystorage',
+ 'access_modes': 'ReadWriteMany'}
+
+ @mock.patch('oc_pvc.Utils.create_tmpfile_copy')
+ @mock.patch('oc_pvc.OCPVC._run')
+ def test_create_pvc(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a pvc create '''
+ params = copy.deepcopy(OCPVCTest.params)
+
+ pvc = '''{"kind": "PersistentVolumeClaim",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "mypvc",
+ "namespace": "test",
+ "selfLink": "/api/v1/namespaces/test/persistentvolumeclaims/mypvc",
+ "uid": "77597898-d8d8-11e6-aea5-0e3c0c633889",
+ "resourceVersion": "126510787",
+ "creationTimestamp": "2017-01-12T15:04:50Z",
+ "labels": {
+ "mypvc": "database"
+ },
+ "annotations": {
+ "pv.kubernetes.io/bind-completed": "yes",
+ "pv.kubernetes.io/bound-by-controller": "yes",
+ "v1.2-volume.experimental.kubernetes.io/provisioning-required": "volume.experimental.kubernetes.io/provisioning-completed"
+ }
+ },
+ "spec": {
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "resources": {
+ "requests": {
+ "storage": "1Gi"
+ }
+ },
+ "selector": {
+ "matchLabels": {
+ "foo": "bar",
+ "abc": "a123"
+ }
+ },
+ "storageClassName": "myStorage",
+ "volumeName": "pv-aws-ow5vl"
+ },
+ "status": {
+ "phase": "Bound",
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "capacity": {
+ "storage": "1Gi"
+ }
+ }
+ }'''
+
+ mock_run.side_effect = [
+ (1, '', 'Error from server: persistentvolumeclaims "mypvc" not found'),
+ (1, '', 'Error from server: persistentvolumeclaims "mypvc" not found'),
+ (0, '', ''),
+ (0, pvc, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCPVC.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'mypvc')
+ self.assertEqual(results['results']['results'][0]['spec']['storageClassName'], 'myStorage')
+ self.assertEqual(results['results']['results'][0]['spec']['selector']['matchLabels']['foo'], 'bar')
+
+ @mock.patch('oc_pvc.Utils.create_tmpfile_copy')
+ @mock.patch('oc_pvc.OCPVC._run')
+ def test_update_pvc(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a pvc create '''
+ params = copy.deepcopy(OCPVCTest.params)
+ params['access_modes'] = 'ReadWriteMany'
+
+ pvc = '''{"kind": "PersistentVolumeClaim",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "mypvc",
+ "namespace": "test",
+ "selfLink": "/api/v1/namespaces/test/persistentvolumeclaims/mypvc",
+ "uid": "77597898-d8d8-11e6-aea5-0e3c0c633889",
+ "resourceVersion": "126510787",
+ "creationTimestamp": "2017-01-12T15:04:50Z",
+ "labels": {
+ "mypvc": "database"
+ },
+ "annotations": {
+ "pv.kubernetes.io/bind-completed": "yes",
+ "pv.kubernetes.io/bound-by-controller": "yes",
+ "v1.2-volume.experimental.kubernetes.io/provisioning-required": "volume.experimental.kubernetes.io/provisioning-completed"
+ }
+ },
+ "spec": {
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "resources": {
+ "requests": {
+ "storage": "1Gi"
+ }
+ },
+ "volumeName": "pv-aws-ow5vl"
+ },
+ "status": {
+ "phase": "Bound",
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "capacity": {
+ "storage": "1Gi"
+ }
+ }
+ }'''
+
+ mod_pvc = '''{"kind": "PersistentVolumeClaim",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "mypvc",
+ "namespace": "test",
+ "selfLink": "/api/v1/namespaces/test/persistentvolumeclaims/mypvc",
+ "uid": "77597898-d8d8-11e6-aea5-0e3c0c633889",
+ "resourceVersion": "126510787",
+ "creationTimestamp": "2017-01-12T15:04:50Z",
+ "labels": {
+ "mypvc": "database"
+ },
+ "annotations": {
+ "pv.kubernetes.io/bind-completed": "yes",
+ "pv.kubernetes.io/bound-by-controller": "yes",
+ "v1.2-volume.experimental.kubernetes.io/provisioning-required": "volume.experimental.kubernetes.io/provisioning-completed"
+ }
+ },
+ "spec": {
+ "accessModes": [
+ "ReadWriteMany"
+ ],
+ "resources": {
+ "requests": {
+ "storage": "1Gi"
+ }
+ },
+ "volumeName": "pv-aws-ow5vl"
+ },
+ "status": {
+ "phase": "Bound",
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "capacity": {
+ "storage": "1Gi"
+ }
+ }
+ }'''
+
+ mock_run.side_effect = [
+ (0, pvc, ''),
+ (0, pvc, ''),
+ (0, '', ''),
+ (0, mod_pvc, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCPVC.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['results']['msg'], '##### - This volume is currently bound. Will not update - ####')
+
+ @mock.patch('oc_pvc.Utils.create_tmpfile_copy')
+ @mock.patch('oc_pvc.OCPVC._run')
+ def test_delete_pvc(self, mock_run, mock_tmpfile_copy):
+ ''' Testing a pvc create '''
+ params = copy.deepcopy(OCPVCTest.params)
+ params['state'] = 'absent'
+
+ pvc = '''{"kind": "PersistentVolumeClaim",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "mypvc",
+ "namespace": "test",
+ "selfLink": "/api/v1/namespaces/test/persistentvolumeclaims/mypvc",
+ "uid": "77597898-d8d8-11e6-aea5-0e3c0c633889",
+ "resourceVersion": "126510787",
+ "creationTimestamp": "2017-01-12T15:04:50Z",
+ "labels": {
+ "mypvc": "database"
+ },
+ "annotations": {
+ "pv.kubernetes.io/bind-completed": "yes",
+ "pv.kubernetes.io/bound-by-controller": "yes",
+ "v1.2-volume.experimental.kubernetes.io/provisioning-required": "volume.experimental.kubernetes.io/provisioning-completed"
+ }
+ },
+ "spec": {
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "resources": {
+ "requests": {
+ "storage": "1Gi"
+ }
+ },
+ "volumeName": "pv-aws-ow5vl"
+ },
+ "status": {
+ "phase": "Bound",
+ "accessModes": [
+ "ReadWriteOnce"
+ ],
+ "capacity": {
+ "storage": "1Gi"
+ }
+ }
+ }'''
+
+ mock_run.side_effect = [
+ (0, pvc, ''),
+ (0, '', ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCPVC.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_route.py b/roles/lib_openshift/src/test/unit/test_oc_route.py
new file mode 100755
index 000000000..5699f123b
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_route.py
@@ -0,0 +1,374 @@
+'''
+ Unit tests for oc route
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_route import OCRoute, locate_oc_binary # noqa: E402
+
+
+class OCRouteTest(unittest.TestCase):
+ '''
+ Test class for OCRoute
+ '''
+
+ @mock.patch('oc_route.locate_oc_binary')
+ @mock.patch('oc_route.Utils.create_tmpfile_copy')
+ @mock.patch('oc_route.OCRoute._run')
+ def test_list_route(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing getting a route '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'list',
+ 'debug': False,
+ 'name': 'test',
+ 'namespace': 'default',
+ 'labels': {'route': 'route'},
+ 'tls_termination': 'passthrough',
+ 'dest_cacert_path': None,
+ 'cacert_path': None,
+ 'cert_path': None,
+ 'key_path': None,
+ 'dest_cacert_content': None,
+ 'cacert_content': None,
+ 'cert_content': None,
+ 'key_content': None,
+ 'service_name': 'testservice',
+ 'host': 'test.openshift.com',
+ 'wildcard_policy': None,
+ 'weight': None,
+ 'port': None
+ }
+
+ route_result = '''{
+ "kind": "Route",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "test",
+ "namespace": "default",
+ "selfLink": "/oapi/v1/namespaces/default/routes/test",
+ "uid": "1b127c67-ecd9-11e6-96eb-0e0d9bdacd26",
+ "resourceVersion": "439182",
+ "creationTimestamp": "2017-02-07T01:59:48Z",
+ "labels": {
+ "route": "route"
+ }
+ },
+ "spec": {
+ "host": "test.example",
+ "to": {
+ "kind": "Service",
+ "name": "test",
+ "weight": 100
+ },
+ "port": {
+ "targetPort": 8443
+ },
+ "tls": {
+ "termination": "passthrough"
+ },
+ "wildcardPolicy": "None"
+ },
+ "status": {
+ "ingress": [
+ {
+ "host": "test.example",
+ "routerName": "router",
+ "conditions": [
+ {
+ "type": "Admitted",
+ "status": "True",
+ "lastTransitionTime": "2017-02-07T01:59:48Z"
+ }
+ ],
+ "wildcardPolicy": "None"
+ }
+ ]
+ }
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (0, route_result, ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock.kubeconfig',
+ ]
+
+ # Act
+ results = OCRoute.run_ansible(params, False)
+
+ # Assert
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['state'], 'list')
+ self.assertEqual(results['results'][0]['metadata']['name'], 'test')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'route', 'test', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @mock.patch('oc_route.locate_oc_binary')
+ @mock.patch('oc_route.Utils.create_tmpfile_copy')
+ @mock.patch('oc_route.Yedit._write')
+ @mock.patch('oc_route.OCRoute._run')
+ def test_create_route(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing getting a route '''
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'present',
+ 'debug': False,
+ 'name': 'test',
+ 'namespace': 'default',
+ 'labels': {'route': 'route'},
+ 'tls_termination': 'edge',
+ 'dest_cacert_path': None,
+ 'cacert_path': None,
+ 'cert_path': None,
+ 'key_path': None,
+ 'dest_cacert_content': None,
+ 'cacert_content': 'testing',
+ 'cert_content': 'testing',
+ 'key_content': 'testing',
+ 'service_name': 'testservice',
+ 'host': 'test.openshift.com',
+ 'wildcard_policy': None,
+ 'weight': None,
+ 'port': None
+ }
+
+ route_result = '''{
+ "apiVersion": "v1",
+ "kind": "Route",
+ "metadata": {
+ "creationTimestamp": "2017-02-07T20:55:10Z",
+ "name": "test",
+ "namespace": "default",
+ "resourceVersion": "517745",
+ "selfLink": "/oapi/v1/namespaces/default/routes/test",
+ "uid": "b6f25898-ed77-11e6-9755-0e737db1e63a",
+ "labels": {"route": "route"}
+ },
+ "spec": {
+ "host": "test.openshift.com",
+ "tls": {
+ "caCertificate": "testing",
+ "certificate": "testing",
+ "key": "testing",
+ "termination": "edge"
+ },
+ "to": {
+ "kind": "Service",
+ "name": "testservice",
+ "weight": 100
+ },
+ "wildcardPolicy": "None"
+ },
+ "status": {
+ "ingress": [
+ {
+ "conditions": [
+ {
+ "lastTransitionTime": "2017-02-07T20:55:10Z",
+ "status": "True",
+ "type": "Admitted"
+ }
+ ],
+ "host": "test.openshift.com",
+ "routerName": "router",
+ "wildcardPolicy": "None"
+ }
+ ]
+ }
+ }'''
+
+ test_route = '''\
+kind: Route
+spec:
+ tls:
+ caCertificate: testing
+ termination: edge
+ certificate: testing
+ key: testing
+ to:
+ kind: Service
+ name: testservice
+ weight: 100
+ host: test.openshift.com
+ wildcardPolicy: None
+apiVersion: v1
+metadata:
+ namespace: default
+ name: test
+'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (1, '', 'Error from server: routes "test" not found'),
+ (1, '', 'Error from server: routes "test" not found'),
+ (0, 'route "test" created', ''),
+ (0, route_result, ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mock.kubeconfig',
+ ]
+
+ mock_write.assert_has_calls = [
+ # First call to mock
+ mock.call('/tmp/test', test_route)
+ ]
+
+ # Act
+ results = OCRoute.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['state'], 'present')
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'test')
+ self.assertEqual(results['results']['results'][0]['metadata']['labels']['route'], 'route')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'route', 'test', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'get', 'route', 'test', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_scale.py b/roles/lib_openshift/src/test/unit/test_oc_scale.py
new file mode 100755
index 000000000..d810735f2
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_scale.py
@@ -0,0 +1,256 @@
+'''
+ Unit tests for oc scale
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_scale import OCScale, locate_oc_binary # noqa: E402
+
+
+class OCScaleTest(unittest.TestCase):
+ '''
+ Test class for OCVersion
+ '''
+
+ @mock.patch('oc_scale.Utils.create_tmpfile_copy')
+ @mock.patch('oc_scale.OCScale.openshift_cmd')
+ def test_state_list(self, mock_openshift_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'name': 'router',
+ 'namespace': 'default',
+ 'replicas': 2,
+ 'state': 'list',
+ 'kind': 'dc',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ dc = '''{"kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "namespace": "default",
+ "selfLink": "/oapi/v1/namespaces/default/deploymentconfigs/router",
+ "uid": "a441eedc-e1ae-11e6-a2d5-0e6967f34d42",
+ "resourceVersion": "6558",
+ "generation": 8,
+ "creationTimestamp": "2017-01-23T20:58:07Z",
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "replicas": 2,
+ }
+ }'''
+
+ mock_openshift_cmd.side_effect = [
+ {"cmd": '/usr/bin/oc get dc router -n default',
+ 'results': dc,
+ 'returncode': 0}]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCScale.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['result'][0], 2)
+
+ @mock.patch('oc_scale.Utils.create_tmpfile_copy')
+ @mock.patch('oc_scale.OCScale.openshift_cmd')
+ def test_scale(self, mock_openshift_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'name': 'router',
+ 'namespace': 'default',
+ 'replicas': 3,
+ 'state': 'list',
+ 'kind': 'dc',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ dc = '''{"kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "namespace": "default",
+ "selfLink": "/oapi/v1/namespaces/default/deploymentconfigs/router",
+ "uid": "a441eedc-e1ae-11e6-a2d5-0e6967f34d42",
+ "resourceVersion": "6558",
+ "generation": 8,
+ "creationTimestamp": "2017-01-23T20:58:07Z",
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "replicas": 3,
+ }
+ }'''
+
+ mock_openshift_cmd.side_effect = [
+ {"cmd": '/usr/bin/oc get dc router -n default',
+ 'results': dc,
+ 'returncode': 0},
+ {"cmd": '/usr/bin/oc create -f /tmp/router -n default',
+ 'results': '',
+ 'returncode': 0}
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCScale.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['result'][0], 3)
+
+ @mock.patch('oc_scale.Utils.create_tmpfile_copy')
+ @mock.patch('oc_scale.OCScale.openshift_cmd')
+ def test_no_dc_scale(self, mock_openshift_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'name': 'not_there',
+ 'namespace': 'default',
+ 'replicas': 3,
+ 'state': 'present',
+ 'kind': 'dc',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ mock_openshift_cmd.side_effect = [
+ {"cmd": '/usr/bin/oc -n default get dc not_there -o json',
+ 'results': [{}],
+ 'returncode': 1,
+ 'stderr': "Error from server: deploymentconfigs \"not_there\" not found\n",
+ 'stdout': ""},
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCScale.run_ansible(params, False)
+
+ self.assertTrue(results['failed'])
+ self.assertEqual(results['msg']['returncode'], 1)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_secret.py b/roles/lib_openshift/src/test/unit/test_oc_secret.py
new file mode 100755
index 000000000..323b3423c
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_secret.py
@@ -0,0 +1,192 @@
+'''
+ Unit tests for oc secret
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_secret import OCSecret, locate_oc_binary # noqa: E402
+
+
+class OCSecretTest(unittest.TestCase):
+ '''
+ Test class for OCSecret
+ '''
+
+ @mock.patch('oc_secret.locate_oc_binary')
+ @mock.patch('oc_secret.Utils.create_tmpfile_copy')
+ @mock.patch('oc_secret.Utils._write')
+ @mock.patch('oc_secret.OCSecret._run')
+ def test_adding_a_secret(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing adding a secret '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'present',
+ 'namespace': 'default',
+ 'name': 'testsecretname',
+ 'type': 'Opaque',
+ 'contents': [{
+ 'path': "/tmp/somesecret.json",
+ 'data': "{'one': 1, 'two': 2, 'three': 3}",
+ }],
+ 'decode': False,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ 'files': None,
+ 'delete_after': True,
+ 'force': False,
+ }
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: secrets "testsecretname" not found'),
+ (0, 'secret/testsecretname', ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ # Act
+ results = OCSecret.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'secrets', 'testsecretname', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'secrets', 'new', 'testsecretname', '--type=Opaque', mock.ANY, '-n', 'default'], None),
+ ])
+
+ mock_write.assert_has_calls([
+ mock.call(mock.ANY, "{'one': 1, 'two': 2, 'three': 3}"),
+ ])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_service.py b/roles/lib_openshift/src/test/unit/test_oc_service.py
new file mode 100755
index 000000000..9c21a262f
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_service.py
@@ -0,0 +1,487 @@
+'''
+ Unit tests for oc service
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_service import OCService, locate_oc_binary # noqa: E402
+
+
+# pylint: disable=too-many-public-methods
+class OCServiceTest(unittest.TestCase):
+ '''
+ Test class for OCService
+ '''
+
+ @mock.patch('oc_service.Utils.create_tmpfile_copy')
+ @mock.patch('oc_service.OCService._run')
+ def test_state_list(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'name': 'router',
+ 'namespace': 'default',
+ 'ports': None,
+ 'state': 'list',
+ 'labels': None,
+ 'clusterip': None,
+ 'portalip': None,
+ 'selector': None,
+ 'session_affinity': None,
+ 'service_type': None,
+ 'external_ips': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ service = '''{
+ "kind": "Service",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/services/router",
+ "uid": "fabd2440-e3d8-11e6-951c-0e3dd518cefa",
+ "resourceVersion": "3206",
+ "creationTimestamp": "2017-01-26T15:06:14Z",
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "80-tcp",
+ "protocol": "TCP",
+ "port": 80,
+ "targetPort": 80
+ },
+ {
+ "name": "443-tcp",
+ "protocol": "TCP",
+ "port": 443,
+ "targetPort": 443
+ },
+ {
+ "name": "1936-tcp",
+ "protocol": "TCP",
+ "port": 1936,
+ "targetPort": 1936
+ },
+ {
+ "name": "5000-tcp",
+ "protocol": "TCP",
+ "port": 5000,
+ "targetPort": 5000
+ }
+ ],
+ "selector": {
+ "router": "router"
+ },
+ "clusterIP": "172.30.129.161",
+ "type": "ClusterIP",
+ "sessionAffinity": "None"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ }'''
+ mock_cmd.side_effect = [
+ (0, service, '')
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCService.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'router')
+
+ @mock.patch('oc_service.Utils.create_tmpfile_copy')
+ @mock.patch('oc_service.OCService._run')
+ def test_create(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a create service '''
+ params = {'name': 'router',
+ 'namespace': 'default',
+ 'ports': {'name': '9000-tcp',
+ 'port': 9000,
+ 'protocol': 'TCP',
+ 'targetPOrt': 9000},
+ 'state': 'present',
+ 'labels': None,
+ 'clusterip': None,
+ 'portalip': None,
+ 'selector': {'router': 'router'},
+ 'session_affinity': 'ClientIP',
+ 'service_type': 'ClusterIP',
+ 'external_ips': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ service = '''{
+ "kind": "Service",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/services/router",
+ "uid": "fabd2440-e3d8-11e6-951c-0e3dd518cefa",
+ "resourceVersion": "3206",
+ "creationTimestamp": "2017-01-26T15:06:14Z",
+ "labels": {
+ "router": "router"
+ }
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "80-tcp",
+ "protocol": "TCP",
+ "port": 80,
+ "targetPort": 80
+ },
+ {
+ "name": "443-tcp",
+ "protocol": "TCP",
+ "port": 443,
+ "targetPort": 443
+ },
+ {
+ "name": "1936-tcp",
+ "protocol": "TCP",
+ "port": 1936,
+ "targetPort": 1936
+ },
+ {
+ "name": "5000-tcp",
+ "protocol": "TCP",
+ "port": 5000,
+ "targetPort": 5000
+ }
+ ],
+ "selector": {
+ "router": "router"
+ },
+ "clusterIP": "172.30.129.161",
+ "type": "ClusterIP",
+ "sessionAffinity": "None"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ }'''
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: services "router" not found'),
+ (1, '', 'Error from server: services "router" not found'),
+ (0, service, ''),
+ (0, service, '')
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCService.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['returncode'] == 0)
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'router')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @mock.patch('oc_service.Utils.create_tmpfile_copy')
+ @mock.patch('oc_service.OCService._run')
+ def test_create_with_labels(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a create service '''
+ params = {'name': 'router',
+ 'namespace': 'default',
+ 'ports': {'name': '9000-tcp',
+ 'port': 9000,
+ 'protocol': 'TCP',
+ 'targetPOrt': 9000},
+ 'state': 'present',
+ 'labels': {'component': 'some_component', 'infra': 'true'},
+ 'clusterip': None,
+ 'portalip': None,
+ 'selector': {'router': 'router'},
+ 'session_affinity': 'ClientIP',
+ 'service_type': 'ClusterIP',
+ 'external_ips': None,
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ service = '''{
+ "kind": "Service",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/services/router",
+ "uid": "fabd2440-e3d8-11e6-951c-0e3dd518cefa",
+ "resourceVersion": "3206",
+ "creationTimestamp": "2017-01-26T15:06:14Z",
+ "labels": {"component": "some_component", "infra": "true"}
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "80-tcp",
+ "protocol": "TCP",
+ "port": 80,
+ "targetPort": 80
+ },
+ {
+ "name": "443-tcp",
+ "protocol": "TCP",
+ "port": 443,
+ "targetPort": 443
+ },
+ {
+ "name": "1936-tcp",
+ "protocol": "TCP",
+ "port": 1936,
+ "targetPort": 1936
+ },
+ {
+ "name": "5000-tcp",
+ "protocol": "TCP",
+ "port": 5000,
+ "targetPort": 5000
+ }
+ ],
+ "selector": {
+ "router": "router"
+ },
+ "clusterIP": "172.30.129.161",
+ "type": "ClusterIP",
+ "sessionAffinity": "None"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ }'''
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: services "router" not found'),
+ (1, '', 'Error from server: services "router" not found'),
+ (0, service, ''),
+ (0, service, '')
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCService.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['returncode'] == 0)
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'router')
+ self.assertEqual(results['results']['results'][0]['metadata']['labels'], {"component": "some_component", "infra": "true"})
+
+ @mock.patch('oc_service.Utils.create_tmpfile_copy')
+ @mock.patch('oc_service.OCService._run')
+ def test_create_with_external_ips(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a create service '''
+ params = {'name': 'router',
+ 'namespace': 'default',
+ 'ports': {'name': '9000-tcp',
+ 'port': 9000,
+ 'protocol': 'TCP',
+ 'targetPOrt': 9000},
+ 'state': 'present',
+ 'labels': {'component': 'some_component', 'infra': 'true'},
+ 'clusterip': None,
+ 'portalip': None,
+ 'selector': {'router': 'router'},
+ 'session_affinity': 'ClientIP',
+ 'service_type': 'ClusterIP',
+ 'external_ips': ['1.2.3.4', '5.6.7.8'],
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False}
+
+ service = '''{
+ "kind": "Service",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "router",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/services/router",
+ "uid": "fabd2440-e3d8-11e6-951c-0e3dd518cefa",
+ "resourceVersion": "3206",
+ "creationTimestamp": "2017-01-26T15:06:14Z",
+ "labels": {"component": "some_component", "infra": "true"}
+ },
+ "spec": {
+ "ports": [
+ {
+ "name": "80-tcp",
+ "protocol": "TCP",
+ "port": 80,
+ "targetPort": 80
+ },
+ {
+ "name": "443-tcp",
+ "protocol": "TCP",
+ "port": 443,
+ "targetPort": 443
+ },
+ {
+ "name": "1936-tcp",
+ "protocol": "TCP",
+ "port": 1936,
+ "targetPort": 1936
+ },
+ {
+ "name": "5000-tcp",
+ "protocol": "TCP",
+ "port": 5000,
+ "targetPort": 5000
+ }
+ ],
+ "selector": {
+ "router": "router"
+ },
+ "clusterIP": "172.30.129.161",
+ "externalIPs": ["1.2.3.4", "5.6.7.8"],
+ "type": "ClusterIP",
+ "sessionAffinity": "None"
+ },
+ "status": {
+ "loadBalancer": {}
+ }
+ }'''
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: services "router" not found'),
+ (1, '', 'Error from server: services "router" not found'),
+ (0, service, ''),
+ (0, service, '')
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCService.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['returncode'] == 0)
+ self.assertEqual(results['results']['results'][0]['metadata']['name'], 'router')
+ self.assertEqual(results['results']['results'][0]['metadata']['labels'], {"component": "some_component", "infra": "true"})
+ self.assertEqual(results['results']['results'][0]['spec']['externalIPs'], ["1.2.3.4", "5.6.7.8"])
diff --git a/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py
new file mode 100755
index 000000000..5772d2f00
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount.py
@@ -0,0 +1,213 @@
+'''
+ Unit tests for oc serviceaccount
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_serviceaccount import OCServiceAccount, locate_oc_binary # noqa: E402
+
+
+class OCServiceAccountTest(unittest.TestCase):
+ '''
+ Test class for OCServiceAccount
+ '''
+
+ @mock.patch('oc_serviceaccount.locate_oc_binary')
+ @mock.patch('oc_serviceaccount.Utils.create_tmpfile_copy')
+ @mock.patch('oc_serviceaccount.OCServiceAccount._run')
+ def test_adding_a_serviceaccount(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing adding a serviceaccount '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'present',
+ 'debug': False,
+ 'name': 'testserviceaccountname',
+ 'namespace': 'default',
+ 'secrets': None,
+ 'image_pull_secrets': None,
+ }
+
+ valid_result_json = '''{
+ "kind": "ServiceAccount",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "testserviceaccountname",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/serviceaccounts/testserviceaccountname",
+ "uid": "4d8320c9-e66f-11e6-8edc-0eece8f2ce22",
+ "resourceVersion": "328450",
+ "creationTimestamp": "2017-01-29T22:07:19Z"
+ },
+ "secrets": [
+ {
+ "name": "testserviceaccountname-dockercfg-4lqd0"
+ },
+ {
+ "name": "testserviceaccountname-token-9h0ej"
+ }
+ ],
+ "imagePullSecrets": [
+ {
+ "name": "testserviceaccountname-dockercfg-4lqd0"
+ }
+ ]
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (1, '', 'Error from server: serviceaccounts "testserviceaccountname" not found'),
+
+ # Second call to mock
+ (0, 'serviceaccount "testserviceaccountname" created', ''),
+
+ # Third call to mock
+ (0, valid_result_json, ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ # Act
+ results = OCServiceAccount.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'sa', 'testserviceaccountname', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'get', 'sa', 'testserviceaccountname', '-o', 'json', '-n', 'default'], None),
+ ])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py
new file mode 100755
index 000000000..b22525068
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_serviceaccount_secret.py
@@ -0,0 +1,414 @@
+'''
+ Unit tests for oc secret add
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_serviceaccount_secret import OCServiceAccountSecret, locate_oc_binary # noqa: E402
+
+try:
+ import ruamel.yaml as yaml # noqa: EF401
+ YAML_TYPE = 'ruamel'
+except ImportError:
+ YAML_TYPE = 'pyyaml'
+
+
+class OCServiceAccountSecretTest(unittest.TestCase):
+ '''
+ Test class for OCServiceAccountSecret
+ '''
+
+ @mock.patch('oc_serviceaccount_secret.locate_oc_binary')
+ @mock.patch('oc_serviceaccount_secret.Utils.create_tmpfile_copy')
+ @mock.patch('oc_serviceaccount_secret.Yedit._write')
+ @mock.patch('oc_serviceaccount_secret.OCServiceAccountSecret._run')
+ def test_adding_a_secret_to_a_serviceaccount(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing adding a secret to a service account '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'present',
+ 'namespace': 'default',
+ 'secret': 'newsecret',
+ 'service_account': 'builder',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ }
+
+ oc_get_sa_before = '''{
+ "apiVersion": "v1",
+ "imagePullSecrets": [
+ {
+ "name": "builder-dockercfg-rsrua"
+ }
+ ],
+ "kind": "ServiceAccount",
+ "metadata": {
+ "name": "builder",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/serviceaccounts/builder",
+ "uid": "cf47bca7-ebc4-11e6-b041-0ed9df7abc38",
+ "resourceVersion": "302879",
+ "creationTimestamp": "2017-02-05T17:02:00Z"
+ },
+ "secrets": [
+ {
+ "name": "builder-dockercfg-rsrua"
+ },
+ {
+ "name": "builder-token-akqxi"
+ }
+
+ ]
+ }
+ '''
+
+ oc_get_sa_after = '''{
+ "apiVersion": "v1",
+ "imagePullSecrets": [
+ {
+ "name": "builder-dockercfg-rsrua"
+ }
+ ],
+ "kind": "ServiceAccount",
+ "metadata": {
+ "name": "builder",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/serviceaccounts/builder",
+ "uid": "cf47bca7-ebc4-11e6-b041-0ed9df7abc38",
+ "resourceVersion": "302879",
+ "creationTimestamp": "2017-02-05T17:02:00Z"
+ },
+ "secrets": [
+ {
+ "name": "builder-dockercfg-rsrua"
+ },
+ {
+ "name": "builder-token-akqxi"
+ },
+ {
+ "name": "newsecret"
+ }
+
+ ]
+ }
+ '''
+ builder_ryaml_file = '''\
+secrets:
+- name: builder-dockercfg-rsrua
+- name: builder-token-akqxi
+- name: newsecret
+kind: ServiceAccount
+imagePullSecrets:
+- name: builder-dockercfg-rsrua
+apiVersion: v1
+metadata:
+ name: builder
+ namespace: default
+ resourceVersion: '302879'
+ creationTimestamp: '2017-02-05T17:02:00Z'
+ selfLink: /api/v1/namespaces/default/serviceaccounts/builder
+ uid: cf47bca7-ebc4-11e6-b041-0ed9df7abc38
+'''
+
+ builder_pyyaml_file = '''\
+apiVersion: v1
+imagePullSecrets:
+- name: builder-dockercfg-rsrua
+kind: ServiceAccount
+metadata:
+ creationTimestamp: '2017-02-05T17:02:00Z'
+ name: builder
+ namespace: default
+ resourceVersion: '302879'
+ selfLink: /api/v1/namespaces/default/serviceaccounts/builder
+ uid: cf47bca7-ebc4-11e6-b041-0ed9df7abc38
+secrets:
+- name: builder-dockercfg-rsrua
+- name: builder-token-akqxi
+- name: newsecret
+'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, oc_get_sa_before, ''), # First call to the mock
+ (0, oc_get_sa_before, ''), # Second call to the mock
+ (0, 'serviceaccount "builder" replaced', ''), # Third call to the mock
+ (0, oc_get_sa_after, ''), # Fourth call to the mock
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ # Act
+ results = OCServiceAccountSecret.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mocks were called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'sa', 'builder', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'sa', 'builder', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'replace', '-f', mock.ANY, '-n', 'default'], None),
+ mock.call(['oc', 'get', 'sa', 'builder', '-o', 'json', '-n', 'default'], None)
+ ])
+
+ yaml_file = builder_pyyaml_file
+
+ if YAML_TYPE == 'ruamel':
+ yaml_file = builder_ryaml_file
+ mock_write.assert_has_calls([
+ mock.call(mock.ANY, yaml_file)
+ ])
+
+ @mock.patch('oc_serviceaccount_secret.locate_oc_binary')
+ @mock.patch('oc_serviceaccount_secret.Utils.create_tmpfile_copy')
+ @mock.patch('oc_serviceaccount_secret.Yedit._write')
+ @mock.patch('oc_serviceaccount_secret.OCServiceAccountSecret._run')
+ def test_removing_a_secret_to_a_serviceaccount(self, mock_cmd, mock_write, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing removing a secret to a service account '''
+
+ # Arrange
+
+ # run_ansible input parameters
+ params = {
+ 'state': 'absent',
+ 'namespace': 'default',
+ 'secret': 'newsecret',
+ 'service_account': 'builder',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'debug': False,
+ }
+
+ oc_get_sa_before = '''{
+ "apiVersion": "v1",
+ "imagePullSecrets": [
+ {
+ "name": "builder-dockercfg-rsrua"
+ }
+ ],
+ "kind": "ServiceAccount",
+ "metadata": {
+ "name": "builder",
+ "namespace": "default",
+ "selfLink": "/api/v1/namespaces/default/serviceaccounts/builder",
+ "uid": "cf47bca7-ebc4-11e6-b041-0ed9df7abc38",
+ "resourceVersion": "302879",
+ "creationTimestamp": "2017-02-05T17:02:00Z"
+ },
+ "secrets": [
+ {
+ "name": "builder-dockercfg-rsrua"
+ },
+ {
+ "name": "builder-token-akqxi"
+ },
+ {
+ "name": "newsecret"
+ }
+
+ ]
+ }
+ '''
+
+ builder_ryaml_file = '''\
+secrets:
+- name: builder-dockercfg-rsrua
+- name: builder-token-akqxi
+kind: ServiceAccount
+imagePullSecrets:
+- name: builder-dockercfg-rsrua
+apiVersion: v1
+metadata:
+ name: builder
+ namespace: default
+ resourceVersion: '302879'
+ creationTimestamp: '2017-02-05T17:02:00Z'
+ selfLink: /api/v1/namespaces/default/serviceaccounts/builder
+ uid: cf47bca7-ebc4-11e6-b041-0ed9df7abc38
+'''
+
+ builder_pyyaml_file = '''\
+apiVersion: v1
+imagePullSecrets:
+- name: builder-dockercfg-rsrua
+kind: ServiceAccount
+metadata:
+ creationTimestamp: '2017-02-05T17:02:00Z'
+ name: builder
+ namespace: default
+ resourceVersion: '302879'
+ selfLink: /api/v1/namespaces/default/serviceaccounts/builder
+ uid: cf47bca7-ebc4-11e6-b041-0ed9df7abc38
+secrets:
+- name: builder-dockercfg-rsrua
+- name: builder-token-akqxi
+'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ (0, oc_get_sa_before, ''), # First call to the mock
+ (0, oc_get_sa_before, ''), # Second call to the mock
+ (0, 'serviceaccount "builder" replaced', ''), # Third call to the mock
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ # Act
+ results = OCServiceAccountSecret.run_ansible(params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['state'], 'absent')
+
+ # Making sure our mocks were called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'sa', 'builder', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'get', 'sa', 'builder', '-o', 'json', '-n', 'default'], None),
+ mock.call(['oc', 'replace', '-f', mock.ANY, '-n', 'default'], None),
+ ])
+
+ yaml_file = builder_pyyaml_file
+
+ if YAML_TYPE == 'ruamel':
+ yaml_file = builder_ryaml_file
+ mock_write.assert_has_calls([
+ mock.call(mock.ANY, yaml_file)
+ ])
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_storageclass.py b/roles/lib_openshift/src/test/unit/test_oc_storageclass.py
new file mode 100755
index 000000000..4fd02a8b1
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_storageclass.py
@@ -0,0 +1,93 @@
+'''
+ Unit tests for oc serviceaccount
+'''
+
+import os
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_storageclass import OCStorageClass # noqa: E402
+
+
+class OCStorageClassTest(unittest.TestCase):
+ '''
+ Test class for OCStorageClass
+ '''
+ params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'present',
+ 'debug': False,
+ 'name': 'testsc',
+ 'provisioner': 'kubernetes.io/aws-ebs',
+ 'annotations': {'storageclass.beta.kubernetes.io/is-default-class': "true"},
+ 'parameters': {'type': 'gp2'},
+ 'api_version': 'v1',
+ 'default_storage_class': 'true'}
+
+ @mock.patch('oc_storageclass.locate_oc_binary')
+ @mock.patch('oc_storageclass.Utils.create_tmpfile_copy')
+ @mock.patch('oc_storageclass.OCStorageClass._run')
+ def test_adding_a_storageclass(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary):
+ ''' Testing adding a storageclass '''
+
+ # Arrange
+
+ # run_ansible input parameters
+
+ valid_result_json = '''{
+ "kind": "StorageClass",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "testsc",
+ "selfLink": "/apis/storage.k8s.io/v1/storageclasses/gp2",
+ "uid": "4d8320c9-e66f-11e6-8edc-0eece8f2ce22",
+ "resourceVersion": "2828",
+ "creationTimestamp": "2017-01-29T22:07:19Z",
+ "annotations": {"storageclass.beta.kubernetes.io/is-default-class": "true"}
+ },
+ "provisioner": "kubernetes.io/aws-ebs",
+ "parameters": {"type": "gp2"}
+ }'''
+
+ # Return values of our mocked function call. These get returned once per call.
+ mock_cmd.side_effect = [
+ # First call to mock
+ (1, '', 'Error from server: storageclass "testsc" not found'),
+
+ # Second call to mock
+ (0, 'storageclass "testsc" created', ''),
+
+ # Third call to mock
+ (0, valid_result_json, ''),
+ ]
+
+ mock_oc_binary.side_effect = [
+ 'oc'
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ # Act
+ results = OCStorageClass.run_ansible(OCStorageClassTest.params, False)
+
+ # Assert
+ self.assertTrue(results['changed'])
+ self.assertEqual(results['results']['returncode'], 0)
+ self.assertEqual(results['state'], 'present')
+
+ # Making sure our mock was called as we expected
+ mock_cmd.assert_has_calls([
+ mock.call(['oc', 'get', 'storageclass', 'testsc', '-o', 'json'], None),
+ mock.call(['oc', 'create', '-f', mock.ANY], None),
+ mock.call(['oc', 'get', 'storageclass', 'testsc', '-o', 'json'], None),
+ ])
diff --git a/roles/lib_openshift/src/test/unit/test_oc_user.py b/roles/lib_openshift/src/test/unit/test_oc_user.py
new file mode 100755
index 000000000..f7a17cc2c
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_user.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python2
+'''
+ Unit tests for oc user
+'''
+# To run
+# ./oc_user.py
+#
+# ..
+# ----------------------------------------------------------------------
+# Ran 2 tests in 0.003s
+#
+# OK
+
+import os
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_user import OCUser # noqa: E402
+
+
+class OCUserTest(unittest.TestCase):
+ '''
+ Test class for OCUser
+ '''
+
+ def setUp(self):
+ ''' setup method will create a file and set to known configuration '''
+ pass
+
+ @mock.patch('oc_user.Utils.create_tmpfile_copy')
+ @mock.patch('oc_user.OCUser._run')
+ def test_state_list(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a user list '''
+ params = {'username': 'testuser@email.com',
+ 'state': 'list',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'full_name': None,
+ 'groups': [],
+ 'debug': False}
+
+ user = '''{
+ "kind": "User",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "testuser@email.com",
+ "selfLink": "/oapi/v1/users/testuser@email.com",
+ "uid": "02fee6c9-f20d-11e6-b83b-12e1a7285e80",
+ "resourceVersion": "38566887",
+ "creationTimestamp": "2017-02-13T16:53:58Z"
+ },
+ "fullName": "Test User",
+ "identities": null,
+ "groups": null
+ }'''
+
+ mock_cmd.side_effect = [
+ (0, user, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCUser.run_ansible(params, False)
+
+ self.assertFalse(results['changed'])
+ self.assertTrue(results['results'][0]['metadata']['name'] == "testuser@email.com")
+
+ @mock.patch('oc_user.Utils.create_tmpfile_copy')
+ @mock.patch('oc_user.OCUser._run')
+ def test_state_present(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a user list '''
+ params = {'username': 'testuser@email.com',
+ 'state': 'present',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'full_name': 'Test User',
+ 'groups': [],
+ 'debug': False}
+
+ created_user = '''{
+ "kind": "User",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "testuser@email.com",
+ "selfLink": "/oapi/v1/users/testuser@email.com",
+ "uid": "8d508039-f224-11e6-b83b-12e1a7285e80",
+ "resourceVersion": "38646241",
+ "creationTimestamp": "2017-02-13T19:42:28Z"
+ },
+ "fullName": "Test User",
+ "identities": null,
+ "groups": null
+ }'''
+
+ mock_cmd.side_effect = [
+ (1, '', 'Error from server: users "testuser@email.com" not found'), # get
+ (1, '', 'Error from server: users "testuser@email.com" not found'), # get
+ (0, 'user "testuser@email.com" created', ''), # create
+ (0, created_user, ''), # get
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCUser.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['results'][0]['metadata']['name'] ==
+ "testuser@email.com")
+
+ def tearDown(self):
+ '''TearDown method'''
+ pass
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/roles/lib_openshift/src/test/unit/test_oc_version.py b/roles/lib_openshift/src/test/unit/test_oc_version.py
new file mode 100755
index 000000000..c287bad0b
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_version.py
@@ -0,0 +1,162 @@
+'''
+ Unit tests for oc version
+'''
+
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error,wrong-import-position
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_version import OCVersion, locate_oc_binary # noqa: E402
+
+
+class OCVersionTest(unittest.TestCase):
+ '''
+ Test class for OCVersion
+ '''
+
+ @mock.patch('oc_version.Utils.create_tmpfile_copy')
+ @mock.patch('oc_version.OCVersion.openshift_cmd')
+ def test_get(self, mock_openshift_cmd, mock_tmpfile_copy):
+ ''' Testing a get '''
+ params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'state': 'list',
+ 'debug': False}
+
+ mock_openshift_cmd.side_effect = [
+ {"cmd": "oc version",
+ "results": "oc v3.4.0.39\nkubernetes v1.4.0+776c994\n" +
+ "features: Basic-Auth GSSAPI Kerberos SPNEGO\n\n" +
+ "Server https://internal.api.opstest.openshift.com" +
+ "openshift v3.4.0.39\n" +
+ "kubernetes v1.4.0+776c994\n",
+ "returncode": 0}
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCVersion.run_ansible(params)
+
+ self.assertFalse(results['changed'])
+ self.assertEqual(results['results']['oc_short'], '3.4')
+ self.assertEqual(results['results']['oc_numeric'], '3.4.0.39')
+ self.assertEqual(results['results']['kubernetes_numeric'], '1.4.0')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/src/test/unit/test_oc_volume.py b/roles/lib_openshift/src/test/unit/test_oc_volume.py
new file mode 100755
index 000000000..d91e22bc7
--- /dev/null
+++ b/roles/lib_openshift/src/test/unit/test_oc_volume.py
@@ -0,0 +1,633 @@
+'''
+ Unit tests for oc volume
+'''
+
+import copy
+import os
+import six
+import sys
+import unittest
+import mock
+
+# Removing invalid variable names for tests so that I can
+# keep them brief
+# pylint: disable=invalid-name,no-name-in-module
+# Disable import-error b/c our libraries aren't loaded in jenkins
+# pylint: disable=import-error
+# place class in our python path
+module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501
+sys.path.insert(0, module_path)
+from oc_volume import OCVolume, locate_oc_binary # noqa: E402
+
+
+class OCVolumeTest(unittest.TestCase):
+ '''
+ Test class for OCVolume
+ '''
+ params = {'name': 'oso-rhel7-zagg-web',
+ 'kubeconfig': '/etc/origin/master/admin.kubeconfig',
+ 'namespace': 'test',
+ 'labels': None,
+ 'state': 'present',
+ 'kind': 'dc',
+ 'mount_path': None,
+ 'secret_name': None,
+ 'mount_type': 'pvc',
+ 'claim_name': 'testclaim',
+ 'claim_size': '1G',
+ 'configmap_name': None,
+ 'vol_name': 'test-volume',
+ 'debug': False}
+
+ @mock.patch('oc_volume.Utils.create_tmpfile_copy')
+ @mock.patch('oc_volume.OCVolume._run')
+ def test_create_pvc(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a label list '''
+ params = copy.deepcopy(OCVolumeTest.params)
+
+ dc = '''{
+ "kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "oso-rhel7-zagg-web",
+ "namespace": "new-monitoring",
+ "selfLink": "/oapi/v1/namespaces/new-monitoring/deploymentconfigs/oso-rhel7-zagg-web",
+ "uid": "f56e9dd2-7c13-11e6-b046-0e8844de0587",
+ "resourceVersion": "137095771",
+ "generation": 4,
+ "creationTimestamp": "2016-09-16T13:46:24Z",
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "name": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "strategy": {
+ "type": "Rolling",
+ "rollingParams": {
+ "updatePeriodSeconds": 1,
+ "intervalSeconds": 1,
+ "timeoutSeconds": 600,
+ "maxUnavailable": "25%",
+ "maxSurge": "25%"
+ },
+ "resources": {}
+ },
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ },
+ {
+ "type": "ImageChange",
+ "imageChangeParams": {
+ "automatic": true,
+ "containerNames": [
+ "oso-rhel7-zagg-web"
+ ],
+ "from": {
+ "kind": "ImageStreamTag",
+ "namespace": "new-monitoring",
+ "name": "oso-rhel7-zagg-web:latest"
+ },
+ "lastTriggeredImage": "notused"
+ }
+ }
+ ],
+ "replicas": 10,
+ "test": false,
+ "selector": {
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "monitoring-secrets",
+ "secret": {
+ "secretName": "monitoring-secrets"
+ }
+ }
+ ],
+ "containers": [
+ {
+ "name": "oso-rhel7-zagg-web",
+ "image": "notused",
+ "resources": {},
+ "volumeMounts": [
+ {
+ "name": "monitoring-secrets",
+ "mountPath": "/secrets"
+ }
+ ],
+ "terminationMessagePath": "/dev/termination-log",
+ "imagePullPolicy": "Always",
+ "securityContext": {
+ "capabilities": {},
+ "privileged": false
+ }
+ }
+ ],
+ "restartPolicy": "Always",
+ "terminationGracePeriodSeconds": 30,
+ "dnsPolicy": "ClusterFirst",
+ "securityContext": {}
+ }
+ }
+ }
+ }'''
+
+ post_dc = '''{
+ "kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "oso-rhel7-zagg-web",
+ "namespace": "new-monitoring",
+ "selfLink": "/oapi/v1/namespaces/new-monitoring/deploymentconfigs/oso-rhel7-zagg-web",
+ "uid": "f56e9dd2-7c13-11e6-b046-0e8844de0587",
+ "resourceVersion": "137095771",
+ "generation": 4,
+ "creationTimestamp": "2016-09-16T13:46:24Z",
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "name": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "strategy": {
+ "type": "Rolling",
+ "rollingParams": {
+ "updatePeriodSeconds": 1,
+ "intervalSeconds": 1,
+ "timeoutSeconds": 600,
+ "maxUnavailable": "25%",
+ "maxSurge": "25%"
+ },
+ "resources": {}
+ },
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ },
+ {
+ "type": "ImageChange",
+ "imageChangeParams": {
+ "automatic": true,
+ "containerNames": [
+ "oso-rhel7-zagg-web"
+ ],
+ "from": {
+ "kind": "ImageStreamTag",
+ "namespace": "new-monitoring",
+ "name": "oso-rhel7-zagg-web:latest"
+ },
+ "lastTriggeredImage": "notused"
+ }
+ }
+ ],
+ "replicas": 10,
+ "test": false,
+ "selector": {
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "monitoring-secrets",
+ "secret": {
+ "secretName": "monitoring-secrets"
+ }
+ },
+ {
+ "name": "test-volume",
+ "persistentVolumeClaim": {
+ "claimName": "testclass",
+ "claimSize": "1G"
+ }
+ }
+ ],
+ "containers": [
+ {
+ "name": "oso-rhel7-zagg-web",
+ "image": "notused",
+ "resources": {},
+ "volumeMounts": [
+ {
+ "name": "monitoring-secrets",
+ "mountPath": "/secrets"
+ },
+ {
+ "name": "test-volume",
+ "mountPath": "/data"
+ }
+ ],
+ "terminationMessagePath": "/dev/termination-log",
+ "imagePullPolicy": "Always",
+ "securityContext": {
+ "capabilities": {},
+ "privileged": false
+ }
+ }
+ ],
+ "restartPolicy": "Always",
+ "terminationGracePeriodSeconds": 30,
+ "dnsPolicy": "ClusterFirst",
+ "securityContext": {}
+ }
+ }
+ }
+ }'''
+
+ mock_cmd.side_effect = [
+ (0, dc, ''),
+ (0, dc, ''),
+ (0, '', ''),
+ (0, post_dc, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCVolume.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['results'][-1]['name'] == 'test-volume')
+
+ @mock.patch('oc_volume.Utils.create_tmpfile_copy')
+ @mock.patch('oc_volume.OCVolume._run')
+ def test_create_configmap(self, mock_cmd, mock_tmpfile_copy):
+ ''' Testing a label list '''
+ params = copy.deepcopy(OCVolumeTest.params)
+ params.update({'mount_path': '/configmap',
+ 'mount_type': 'configmap',
+ 'configmap_name': 'configtest',
+ 'vol_name': 'configvol'})
+
+ dc = '''{
+ "kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "oso-rhel7-zagg-web",
+ "namespace": "new-monitoring",
+ "selfLink": "/oapi/v1/namespaces/new-monitoring/deploymentconfigs/oso-rhel7-zagg-web",
+ "uid": "f56e9dd2-7c13-11e6-b046-0e8844de0587",
+ "resourceVersion": "137095771",
+ "generation": 4,
+ "creationTimestamp": "2016-09-16T13:46:24Z",
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "name": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "strategy": {
+ "type": "Rolling",
+ "rollingParams": {
+ "updatePeriodSeconds": 1,
+ "intervalSeconds": 1,
+ "timeoutSeconds": 600,
+ "maxUnavailable": "25%",
+ "maxSurge": "25%"
+ },
+ "resources": {}
+ },
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ },
+ {
+ "type": "ImageChange",
+ "imageChangeParams": {
+ "automatic": true,
+ "containerNames": [
+ "oso-rhel7-zagg-web"
+ ],
+ "from": {
+ "kind": "ImageStreamTag",
+ "namespace": "new-monitoring",
+ "name": "oso-rhel7-zagg-web:latest"
+ },
+ "lastTriggeredImage": "notused"
+ }
+ }
+ ],
+ "replicas": 10,
+ "test": false,
+ "selector": {
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "monitoring-secrets",
+ "secret": {
+ "secretName": "monitoring-secrets"
+ }
+ }
+ ],
+ "containers": [
+ {
+ "name": "oso-rhel7-zagg-web",
+ "image": "notused",
+ "resources": {},
+ "volumeMounts": [
+ {
+ "name": "monitoring-secrets",
+ "mountPath": "/secrets"
+ }
+ ],
+ "terminationMessagePath": "/dev/termination-log",
+ "imagePullPolicy": "Always",
+ "securityContext": {
+ "capabilities": {},
+ "privileged": false
+ }
+ }
+ ],
+ "restartPolicy": "Always",
+ "terminationGracePeriodSeconds": 30,
+ "dnsPolicy": "ClusterFirst",
+ "securityContext": {}
+ }
+ }
+ }
+ }'''
+
+ post_dc = '''{
+ "kind": "DeploymentConfig",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "oso-rhel7-zagg-web",
+ "namespace": "new-monitoring",
+ "selfLink": "/oapi/v1/namespaces/new-monitoring/deploymentconfigs/oso-rhel7-zagg-web",
+ "uid": "f56e9dd2-7c13-11e6-b046-0e8844de0587",
+ "resourceVersion": "137095771",
+ "generation": 4,
+ "creationTimestamp": "2016-09-16T13:46:24Z",
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "name": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "strategy": {
+ "type": "Rolling",
+ "rollingParams": {
+ "updatePeriodSeconds": 1,
+ "intervalSeconds": 1,
+ "timeoutSeconds": 600,
+ "maxUnavailable": "25%",
+ "maxSurge": "25%"
+ },
+ "resources": {}
+ },
+ "triggers": [
+ {
+ "type": "ConfigChange"
+ },
+ {
+ "type": "ImageChange",
+ "imageChangeParams": {
+ "automatic": true,
+ "containerNames": [
+ "oso-rhel7-zagg-web"
+ ],
+ "from": {
+ "kind": "ImageStreamTag",
+ "namespace": "new-monitoring",
+ "name": "oso-rhel7-zagg-web:latest"
+ },
+ "lastTriggeredImage": "notused"
+ }
+ }
+ ],
+ "replicas": 10,
+ "test": false,
+ "selector": {
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "template": {
+ "metadata": {
+ "creationTimestamp": null,
+ "labels": {
+ "app": "oso-rhel7-ops-base",
+ "deploymentconfig": "oso-rhel7-zagg-web"
+ },
+ "annotations": {
+ "openshift.io/generated-by": "OpenShiftNewApp"
+ }
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "monitoring-secrets",
+ "secret": {
+ "secretName": "monitoring-secrets"
+ }
+ },
+ {
+ "name": "configvol",
+ "configMap": {
+ "name": "configtest"
+ }
+ }
+ ],
+ "containers": [
+ {
+ "name": "oso-rhel7-zagg-web",
+ "image": "notused",
+ "resources": {},
+ "volumeMounts": [
+ {
+ "name": "monitoring-secrets",
+ "mountPath": "/secrets"
+ },
+ {
+ "name": "configvol",
+ "mountPath": "/configmap"
+ }
+ ],
+ "terminationMessagePath": "/dev/termination-log",
+ "imagePullPolicy": "Always",
+ "securityContext": {
+ "capabilities": {},
+ "privileged": false
+ }
+ }
+ ],
+ "restartPolicy": "Always",
+ "terminationGracePeriodSeconds": 30,
+ "dnsPolicy": "ClusterFirst",
+ "securityContext": {}
+ }
+ }
+ }
+ }'''
+
+ mock_cmd.side_effect = [
+ (0, dc, ''),
+ (0, dc, ''),
+ (0, '', ''),
+ (0, post_dc, ''),
+ ]
+
+ mock_tmpfile_copy.side_effect = [
+ '/tmp/mocked_kubeconfig',
+ ]
+
+ results = OCVolume.run_ansible(params, False)
+
+ self.assertTrue(results['changed'])
+ self.assertTrue(results['results']['results'][-1]['name'] == 'configvol')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_path_exists.side_effect = lambda _: False
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY3, 'py2 test only')
+ @mock.patch('os.path.exists')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home(self, mock_env_get, mock_path_exists):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_path_exists.side_effect = lambda f: f == oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_fallback_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup fallback '''
+
+ mock_env_get.side_effect = lambda _v, _d: ''
+
+ mock_shutil_which.side_effect = lambda _f, path=None: None
+
+ self.assertEqual(locate_oc_binary(), 'oc')
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_path_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in path '''
+
+ oc_bin = '/usr/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_usr_local_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in /usr/local/bin '''
+
+ oc_bin = '/usr/local/bin/oc'
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
+
+ @unittest.skipIf(six.PY2, 'py3 test only')
+ @mock.patch('shutil.which')
+ @mock.patch('os.environ.get')
+ def test_binary_lookup_in_home_py3(self, mock_env_get, mock_shutil_which):
+ ''' Testing binary lookup in ~/bin '''
+
+ oc_bin = os.path.expanduser('~/bin/oc')
+
+ mock_env_get.side_effect = lambda _v, _d: '/bin:/usr/bin'
+
+ mock_shutil_which.side_effect = lambda _f, path=None: oc_bin
+
+ self.assertEqual(locate_oc_binary(), oc_bin)
diff --git a/roles/lib_openshift/tasks/main.yml b/roles/lib_openshift/tasks/main.yml
new file mode 100644
index 000000000..ed97d539c
--- /dev/null
+++ b/roles/lib_openshift/tasks/main.yml
@@ -0,0 +1 @@
+---