| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 1 | """ | 
|  | 2 | Module to handle interaction with Kube | 
|  | 3 | """ | 
|  | 4 | import base64 | 
|  | 5 | import os | 
|  | 6 | import urllib3 | 
|  | 7 | import yaml | 
|  | 8 |  | 
|  | 9 | from kubernetes import client as kclient, config as kconfig | 
|  | 10 | from kubernetes.stream import stream | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 11 | from kubernetes.client.rest import ApiException | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 12 |  | 
|  | 13 | from cfg_checker.common import logger, logger_cli | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 14 | from cfg_checker.common.decorators import retry | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 15 | from cfg_checker.common.exception import InvalidReturnException, KubeException | 
|  | 16 | from cfg_checker.common.file_utils import create_temp_file_with_content | 
|  | 17 | from cfg_checker.common.other import utils, shell | 
|  | 18 | from cfg_checker.common.ssh_utils import ssh_shell_p | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 19 | from cfg_checker.common.const import ENV_LOCAL | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 20 |  | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 21 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 22 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | 
|  | 23 |  | 
|  | 24 |  | 
|  | 25 | def _init_kube_conf_local(config): | 
|  | 26 | # Init kube library locally | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 27 | _path = "local:{}".format(config.kube_config_path) | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 28 | try: | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 29 | kconfig.load_kube_config(config_file=config.kube_config_path) | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 30 | if config.insecure: | 
|  | 31 | kconfig.assert_hostname = False | 
|  | 32 | kconfig.client_side_validation = False | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 33 | logger_cli.debug( | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 34 | "... found Kube env: core, {}". format( | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 35 | ",".join( | 
|  | 36 | kclient.CoreApi().get_api_versions().versions | 
|  | 37 | ) | 
|  | 38 | ) | 
|  | 39 | ) | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 40 | return kconfig, kclient.ApiClient(), _path | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 41 | except Exception as e: | 
|  | 42 | logger.warn("Failed to init local Kube client: {}".format( | 
|  | 43 | str(e) | 
|  | 44 | ) | 
|  | 45 | ) | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 46 | return None, None, _path | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 47 |  | 
|  | 48 |  | 
|  | 49 | def _init_kube_conf_remote(config): | 
|  | 50 | # init remote client | 
|  | 51 | # Preload Kube token | 
|  | 52 | """ | 
|  | 53 | APISERVER=$(kubectl config view --minify | | 
|  | 54 | grep server | cut -f 2- -d ":" | tr -d " ") | 
|  | 55 | SECRET_NAME=$(kubectl get secrets | | 
|  | 56 | grep ^default | cut -f1 -d ' ') | 
|  | 57 | TOKEN=$(kubectl describe secret $SECRET_NAME | | 
|  | 58 | grep -E '^token' | cut -f2 -d':' | tr -d " ") | 
|  | 59 |  | 
|  | 60 | echo "Detected API Server at: '${APISERVER}'" | 
|  | 61 | echo "Got secret: '${SECRET_NAME}'" | 
|  | 62 | echo "Loaded token: '${TOKEN}'" | 
|  | 63 |  | 
|  | 64 | curl $APISERVER/api | 
|  | 65 | --header "Authorization: Bearer $TOKEN" --insecure | 
|  | 66 | """ | 
|  | 67 | import yaml | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 68 | _path = '' | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 69 | # Try to load remote config only if it was not detected already | 
|  | 70 | if not config.kube_config_detected and not config.env_name == ENV_LOCAL: | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 71 | _path = "{}@{}:{}".format( | 
|  | 72 | config.ssh_user, | 
|  | 73 | config.ssh_host, | 
|  | 74 | config.kube_config_path | 
|  | 75 | ) | 
| Alex | 9d91353 | 2021-03-24 18:01:45 -0500 | [diff] [blame] | 76 | _c_data = ssh_shell_p( | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 77 | "cat " + config.kube_config_path, | 
| Alex | 9d91353 | 2021-03-24 18:01:45 -0500 | [diff] [blame] | 78 | config.ssh_host, | 
|  | 79 | username=config.ssh_user, | 
|  | 80 | keypath=config.ssh_key, | 
|  | 81 | piped=False, | 
|  | 82 | use_sudo=config.ssh_uses_sudo, | 
|  | 83 | ) | 
|  | 84 | else: | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 85 | _path = "local:{}".format(config.kube_config_path) | 
| Alex | 9d91353 | 2021-03-24 18:01:45 -0500 | [diff] [blame] | 86 | with open(config.kube_config_path, 'r') as ff: | 
|  | 87 | _c_data = ff.read() | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 88 |  | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 89 | if len(_c_data) < 1: | 
|  | 90 | return None, None, _path | 
|  | 91 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 92 | _conf = yaml.load(_c_data, Loader=yaml.SafeLoader) | 
|  | 93 |  | 
|  | 94 | _kube_conf = kclient.Configuration() | 
|  | 95 | # A remote host configuration | 
|  | 96 |  | 
|  | 97 | # To work with remote cluster, we need to extract these | 
|  | 98 | # keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl'] | 
|  | 99 | # When v12 of the client will be release, we will use load_from_dict | 
|  | 100 |  | 
|  | 101 | _kube_conf.ssl_ca_cert = create_temp_file_with_content( | 
|  | 102 | base64.standard_b64decode( | 
|  | 103 | _conf['clusters'][0]['cluster']['certificate-authority-data'] | 
|  | 104 | ) | 
|  | 105 | ) | 
|  | 106 | _host = _conf['clusters'][0]['cluster']['server'] | 
|  | 107 | _kube_conf.cert_file = create_temp_file_with_content( | 
|  | 108 | base64.standard_b64decode( | 
|  | 109 | _conf['users'][0]['user']['client-certificate-data'] | 
|  | 110 | ) | 
|  | 111 | ) | 
|  | 112 | _kube_conf.key_file = create_temp_file_with_content( | 
|  | 113 | base64.standard_b64decode( | 
|  | 114 | _conf['users'][0]['user']['client-key-data'] | 
|  | 115 | ) | 
|  | 116 | ) | 
|  | 117 | if "http" not in _host or "443" not in _host: | 
|  | 118 | logger_cli.error( | 
|  | 119 | "Failed to extract Kube host: '{}'".format(_host) | 
|  | 120 | ) | 
|  | 121 | else: | 
|  | 122 | logger_cli.debug( | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 123 | "... 'context' host extracted: '{}' via SSH@{}".format( | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 124 | _host, | 
|  | 125 | config.ssh_host | 
|  | 126 | ) | 
|  | 127 | ) | 
|  | 128 |  | 
|  | 129 | # Substitute context host to ours | 
|  | 130 | _tmp = _host.split(':') | 
|  | 131 | _kube_conf.host = \ | 
|  | 132 | _tmp[0] + "://" + config.mcp_host + ":" + _tmp[2] | 
|  | 133 | config.kube_port = _tmp[2] | 
|  | 134 | logger_cli.debug( | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 135 | "... kube remote host updated to {}".format( | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 136 | _kube_conf.host | 
|  | 137 | ) | 
|  | 138 | ) | 
|  | 139 | _kube_conf.verify_ssl = False | 
|  | 140 | _kube_conf.debug = config.debug | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 141 | if config.insecure: | 
|  | 142 | _kube_conf.assert_hostname = False | 
|  | 143 | _kube_conf.client_side_validation = False | 
|  | 144 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 145 | # Nevertheless if you want to do it | 
|  | 146 | # you can with these 2 parameters | 
|  | 147 | # configuration.verify_ssl=True | 
|  | 148 | # ssl_ca_cert is the filepath | 
|  | 149 | # to the file that contains the certificate. | 
|  | 150 | # configuration.ssl_ca_cert="certificate" | 
|  | 151 |  | 
|  | 152 | # _kube_conf.api_key = { | 
|  | 153 | #     "authorization": "Bearer " + config.kube_token | 
|  | 154 | # } | 
|  | 155 |  | 
|  | 156 | # Create a ApiClient with our config | 
|  | 157 | _kube_api = kclient.ApiClient(_kube_conf) | 
|  | 158 |  | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 159 | return _kube_conf, _kube_api, _path | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 160 |  | 
|  | 161 |  | 
|  | 162 | class KubeApi(object): | 
|  | 163 | def __init__(self, config): | 
|  | 164 | self.config = config | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 165 | self.initialized = self._init_kclient() | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 166 | self.last_response = None | 
|  | 167 |  | 
|  | 168 | def _init_kclient(self): | 
|  | 169 | # if there is no password - try to get local, if this available | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 170 | logger_cli.debug("... init kube config") | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 171 | if self.config.env_name == "local": | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 172 | self.kConf, self.kApi, self.kConfigPath = _init_kube_conf_local( | 
|  | 173 | self.config | 
|  | 174 | ) | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 175 | self.is_local = True | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 176 | # Try to load local config data | 
|  | 177 | if self.config.kube_config_path and \ | 
|  | 178 | os.path.exists(self.config.kube_config_path): | 
|  | 179 | _cmd = "cat " + self.config.kube_config_path | 
|  | 180 | _c_data = shell(_cmd) | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 181 | _conf = yaml.load(_c_data, Loader=yaml.SafeLoader) | 
|  | 182 | self.user_keypath = create_temp_file_with_content( | 
|  | 183 | base64.standard_b64decode( | 
|  | 184 | _conf['users'][0]['user']['client-key-data'] | 
|  | 185 | ) | 
|  | 186 | ) | 
|  | 187 | self.yaml_conf = _c_data | 
|  | 188 | else: | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 189 | self.kConf, self.kApi, self.kConfigPath = _init_kube_conf_remote( | 
|  | 190 | self.config | 
|  | 191 | ) | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 192 | self.is_local = False | 
|  | 193 |  | 
| Alex | 359e575 | 2021-08-16 17:28:30 -0500 | [diff] [blame] | 194 | if self.kConf is None or self.kApi is None: | 
|  | 195 | return False | 
|  | 196 | else: | 
|  | 197 | return True | 
|  | 198 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 199 | def get_versions_api(self): | 
|  | 200 | # client.CoreApi().get_api_versions().versions | 
|  | 201 | return kclient.VersionApi(self.kApi) | 
|  | 202 |  | 
|  | 203 |  | 
|  | 204 | class KubeRemote(KubeApi): | 
|  | 205 | def __init__(self, config): | 
|  | 206 | super(KubeRemote, self).__init__(config) | 
|  | 207 | self._coreV1 = None | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 208 | self._appsV1 = None | 
|  | 209 | self._podV1 = None | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 210 | self._custom = None | 
|  | 211 |  | 
|  | 212 | @property | 
|  | 213 | def CustomObjects(self): | 
|  | 214 | if not self._custom: | 
|  | 215 | self._custom = kclient.CustomObjectsApi(self.kApi) | 
|  | 216 | return self._custom | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 217 |  | 
|  | 218 | @property | 
|  | 219 | def CoreV1(self): | 
|  | 220 | if not self._coreV1: | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 221 | if self.is_local: | 
|  | 222 | self._coreV1 = kclient.CoreV1Api(kclient.ApiClient()) | 
|  | 223 | else: | 
|  | 224 | self._coreV1 = kclient.CoreV1Api(kclient.ApiClient(self.kConf)) | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 225 | return self._coreV1 | 
|  | 226 |  | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 227 | @property | 
|  | 228 | def AppsV1(self): | 
|  | 229 | if not self._appsV1: | 
|  | 230 | self._appsV1 = kclient.AppsV1Api(self.kApi) | 
|  | 231 | return self._appsV1 | 
|  | 232 |  | 
|  | 233 | @property | 
|  | 234 | def PodsV1(self): | 
|  | 235 | if not self._podsV1: | 
|  | 236 | self._podsV1 = kclient.V1Pod(self.kApi) | 
|  | 237 | return self._podsV1 | 
|  | 238 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 239 | @staticmethod | 
|  | 240 | def _typed_list_to_dict(i_list): | 
|  | 241 | _dict = {} | 
|  | 242 | for _item in i_list: | 
|  | 243 | _d = _item.to_dict() | 
|  | 244 | _type = _d.pop("type") | 
|  | 245 | _dict[_type.lower()] = _d | 
|  | 246 |  | 
|  | 247 | return _dict | 
|  | 248 |  | 
|  | 249 | @staticmethod | 
|  | 250 | def _get_listed_attrs(items, _path): | 
|  | 251 | _list = [] | 
|  | 252 | for _n in items: | 
|  | 253 | _list.append(utils.rgetattr(_n, _path)) | 
|  | 254 |  | 
|  | 255 | return _list | 
|  | 256 |  | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 257 | @staticmethod | 
|  | 258 | def safe_get_item_by_name(api_resource, _name): | 
|  | 259 | for item in api_resource.items: | 
|  | 260 | if item.metadata.name == _name: | 
|  | 261 | return item | 
|  | 262 |  | 
|  | 263 | return None | 
|  | 264 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 265 | def get_node_info(self, http=False): | 
|  | 266 | # Query API for the nodes and do some presorting | 
|  | 267 | _nodes = {} | 
|  | 268 | if http: | 
|  | 269 | _raw_nodes = self.CoreV1.list_node_with_http_info() | 
|  | 270 | else: | 
|  | 271 | _raw_nodes = self.CoreV1.list_node() | 
|  | 272 |  | 
|  | 273 | if not isinstance(_raw_nodes, kclient.models.v1_node_list.V1NodeList): | 
|  | 274 | raise InvalidReturnException( | 
|  | 275 | "Invalid return type: '{}'".format(type(_raw_nodes)) | 
|  | 276 | ) | 
|  | 277 |  | 
|  | 278 | for _n in _raw_nodes.items: | 
|  | 279 | _name = _n.metadata.name | 
|  | 280 | _d = _n.to_dict() | 
|  | 281 | # parse inner data classes as dicts | 
|  | 282 | _d['addresses'] = self._typed_list_to_dict(_n.status.addresses) | 
|  | 283 | _d['conditions'] = self._typed_list_to_dict(_n.status.conditions) | 
|  | 284 | # Update 'status' type | 
|  | 285 | if isinstance(_d['conditions']['ready']['status'], str): | 
|  | 286 | _d['conditions']['ready']['status'] = utils.to_bool( | 
|  | 287 | _d['conditions']['ready']['status'] | 
|  | 288 | ) | 
|  | 289 | # Parse image names? | 
|  | 290 | # TODO: Here is the place where we can parse each node image names | 
|  | 291 |  | 
|  | 292 | # Parse roles | 
|  | 293 | _d['labels'] = {} | 
|  | 294 | for _label, _data in _d["metadata"]["labels"].items(): | 
|  | 295 | if _data.lower() in ["true", "false"]: | 
|  | 296 | _d['labels'][_label] = utils.to_bool(_data) | 
|  | 297 | else: | 
|  | 298 | _d['labels'][_label] = _data | 
|  | 299 |  | 
|  | 300 | # Save | 
|  | 301 | _nodes[_name] = _d | 
|  | 302 |  | 
|  | 303 | # debug report on how many nodes detected | 
|  | 304 | logger_cli.debug("...node items returned '{}'".format(len(_nodes))) | 
|  | 305 |  | 
|  | 306 | return _nodes | 
|  | 307 |  | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 308 | def get_pod_names_by_partial_name(self, partial_name, ns): | 
|  | 309 | logger_cli.debug('... searching for pods with {}'.format(partial_name)) | 
|  | 310 | _pods = self.CoreV1.list_namespaced_pod(ns) | 
|  | 311 | _names = self._get_listed_attrs(_pods.items, "metadata.name") | 
|  | 312 | _pnames = [n for n in _names if partial_name in n] | 
|  | 313 | if len(_pnames) > 1: | 
|  | 314 | logger_cli.debug( | 
|  | 315 | "... more than one pod found for '{}': {}\n".format( | 
|  | 316 | partial_name, | 
|  | 317 | ", ".join(_pnames) | 
|  | 318 | ) | 
|  | 319 | ) | 
|  | 320 | elif len(_pnames) < 1: | 
|  | 321 | logger_cli.warning( | 
|  | 322 | "WARNING: No pods found for '{}'".format(partial_name) | 
|  | 323 | ) | 
|  | 324 |  | 
|  | 325 | return _pnames | 
|  | 326 |  | 
|  | 327 | def get_pods_by_partial_name(self, partial_name, ns): | 
|  | 328 | logger_cli.debug('... searching for pods with {}'.format(partial_name)) | 
|  | 329 | _all_pods = self.CoreV1.list_namespaced_pod(ns) | 
|  | 330 | # _names = self._get_listed_attrs(_pods.items, "metadata.name") | 
|  | 331 | _pods = [_pod for _pod in _all_pods.items | 
|  | 332 | if partial_name in _pod.metadata.name] | 
|  | 333 | if len(_pods) > 1: | 
|  | 334 | logger_cli.debug( | 
|  | 335 | "... more than one pod found for '{}': {}\n".format( | 
|  | 336 | partial_name, | 
|  | 337 | ", ".join(partial_name) | 
|  | 338 | ) | 
|  | 339 | ) | 
|  | 340 | elif len(_pods) < 1: | 
|  | 341 | logger_cli.warning( | 
|  | 342 | "WARNING: No pods found for '{}'".format(partial_name) | 
|  | 343 | ) | 
|  | 344 |  | 
|  | 345 | return _pods | 
|  | 346 |  | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 347 | def exec_on_target_pod( | 
|  | 348 | self, | 
|  | 349 | cmd, | 
|  | 350 | pod_name, | 
|  | 351 | namespace, | 
|  | 352 | strict=False, | 
|  | 353 | _request_timeout=120, | 
| Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame^] | 354 | arguments=None, | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 355 | **kwargs | 
|  | 356 | ): | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 357 | _pname = "" | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 358 | if not strict: | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 359 | logger_cli.debug( | 
|  | 360 | "... searching for pods with the name '{}'".format(pod_name) | 
|  | 361 | ) | 
|  | 362 | _pods = {} | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 363 | _pods = self.CoreV1.list_namespaced_pod(namespace) | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 364 | _names = self._get_listed_attrs(_pods.items, "metadata.name") | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 365 | _pnames = [n for n in _names if n.startswith(pod_name)] | 
|  | 366 | if len(_pnames) > 1: | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 367 | logger_cli.debug( | 
| Alex | c4f5962 | 2021-08-27 13:42:00 -0500 | [diff] [blame] | 368 | "... more than one pod found for '{}': {}\n" | 
|  | 369 | "... using first one".format( | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 370 | pod_name, | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 371 | ", ".join(_pnames) | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 372 | ) | 
|  | 373 | ) | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 374 | elif len(_pnames) < 1: | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 375 | raise KubeException("No pods found for '{}'".format(pod_name)) | 
| Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame^] | 376 | # in case of >1 and =1 we are taking 1st anyway | 
|  | 377 | _pname = _pnames[0] | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 378 | else: | 
|  | 379 | _pname = pod_name | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 380 | logger_cli.debug( | 
| Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame^] | 381 | "... cmd: [CoreV1] exec {} -n {} -- {} '{}'".format( | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 382 | _pname, | 
|  | 383 | namespace, | 
| Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame^] | 384 | cmd, | 
|  | 385 | arguments | 
| Alex | 3374781 | 2021-04-07 10:11:39 -0500 | [diff] [blame] | 386 | ) | 
|  | 387 | ) | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 388 | # Set preload_content to False to preserve JSON | 
|  | 389 | # If not, output gets converted to str | 
|  | 390 | # Which causes to change " to ' | 
|  | 391 | # After that json.loads(...) fail | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 392 | cmd = cmd if isinstance(cmd, list) else cmd.split() | 
| Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame^] | 393 | if arguments: | 
|  | 394 | cmd += [arguments] | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 395 | _pod_stream = stream( | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 396 | self.CoreV1.connect_get_namespaced_pod_exec, | 
|  | 397 | _pname, | 
|  | 398 | namespace, | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 399 | command=cmd, | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 400 | stderr=True, | 
|  | 401 | stdin=False, | 
|  | 402 | stdout=True, | 
|  | 403 | tty=False, | 
|  | 404 | _request_timeout=_request_timeout, | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 405 | _preload_content=False, | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 406 | **kwargs | 
|  | 407 | ) | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 408 | # run for timeout | 
|  | 409 | _pod_stream.run_forever(timeout=_request_timeout) | 
|  | 410 | # read the output | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 411 | _output = _pod_stream.read_stdout() | 
| Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame^] | 412 | _error = _pod_stream.read_stderr() | 
|  | 413 | if _error: | 
|  | 414 | # copy error to output | 
|  | 415 | logger_cli.warning( | 
|  | 416 | "WARNING: cmd of '{}' returned error:\n{}\n".format( | 
|  | 417 | " ".join(cmd), | 
|  | 418 | _error | 
|  | 419 | ) | 
|  | 420 | ) | 
|  | 421 | if not _output: | 
|  | 422 | _output = _error | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 423 | # Force recreate of api objects | 
|  | 424 | self._coreV1 = None | 
|  | 425 | # Send output | 
|  | 426 | return _output | 
| Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 427 |  | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 428 | def ensure_namespace(self, ns): | 
|  | 429 | """ | 
|  | 430 | Ensure that given namespace exists | 
|  | 431 | """ | 
|  | 432 | # list active namespaces | 
|  | 433 | _v1NamespaceList = self.CoreV1.list_namespace() | 
|  | 434 | _ns = self.safe_get_item_by_name(_v1NamespaceList, ns) | 
|  | 435 |  | 
|  | 436 | if _ns is None: | 
|  | 437 | logger_cli.debug("... creating namespace '{}'".format(ns)) | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 438 | _new_ns = kclient.V1Namespace() | 
|  | 439 | _new_ns.metadata = kclient.V1ObjectMeta(name=ns) | 
|  | 440 | _r = self.CoreV1.create_namespace(_new_ns) | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 441 | # TODO: check return on fail | 
|  | 442 | if not _r: | 
|  | 443 | return False | 
|  | 444 | else: | 
|  | 445 | logger_cli.debug("... found existing namespace '{}'".format(ns)) | 
|  | 446 |  | 
|  | 447 | return True | 
|  | 448 |  | 
|  | 449 | def get_daemon_set_by_name(self, ns, name): | 
|  | 450 | return self.safe_get_item_by_name( | 
|  | 451 | self.AppsV1.list_namespaced_daemon_set(ns), | 
|  | 452 | name | 
|  | 453 | ) | 
|  | 454 |  | 
|  | 455 | def create_config_map(self, ns, name, source, recreate=True): | 
|  | 456 | """ | 
|  | 457 | Creates/Overwrites ConfigMap in working namespace | 
|  | 458 | """ | 
|  | 459 | # Prepare source | 
|  | 460 | logger_cli.debug( | 
|  | 461 | "... preparing config map '{}/{}' with files from '{}'".format( | 
|  | 462 | ns, | 
|  | 463 | name, | 
|  | 464 | source | 
|  | 465 | ) | 
|  | 466 | ) | 
|  | 467 | _data = {} | 
|  | 468 | if os.path.isfile(source): | 
|  | 469 | # populate data with one file | 
|  | 470 | with open(source, 'rt') as fS: | 
|  | 471 | _data[os.path.split(source)[1]] = fS.read() | 
|  | 472 | elif os.path.isdir(source): | 
|  | 473 | # walk dirs and populate all 'py' files | 
|  | 474 | for path, dirs, files in os.walk(source): | 
|  | 475 | _e = ('.py') | 
|  | 476 | _subfiles = (_fl for _fl in files | 
|  | 477 | if _fl.endswith(_e) and not _fl.startswith('.')) | 
|  | 478 | for _file in _subfiles: | 
|  | 479 | with open(os.path.join(path, _file), 'rt') as fS: | 
|  | 480 | _data[_file] = fS.read() | 
|  | 481 |  | 
|  | 482 | _cm = kclient.V1ConfigMap() | 
|  | 483 | _cm.metadata = kclient.V1ObjectMeta(name=name, namespace=ns) | 
|  | 484 | _cm.data = _data | 
|  | 485 | logger_cli.debug( | 
|  | 486 | "... prepared config map with {} scripts".format(len(_data)) | 
|  | 487 | ) | 
|  | 488 | # Query existing configmap, delete if needed | 
|  | 489 | _existing_cm = self.safe_get_item_by_name( | 
|  | 490 | self.CoreV1.list_namespaced_config_map(namespace=ns), | 
|  | 491 | name | 
|  | 492 | ) | 
|  | 493 | if _existing_cm is not None: | 
|  | 494 | self.CoreV1.replace_namespaced_config_map( | 
|  | 495 | namespace=ns, | 
|  | 496 | name=name, | 
|  | 497 | body=_cm | 
|  | 498 | ) | 
|  | 499 | logger_cli.debug( | 
|  | 500 | "... replaced existing config map '{}/{}'".format( | 
|  | 501 | ns, | 
|  | 502 | name | 
|  | 503 | ) | 
|  | 504 | ) | 
|  | 505 | else: | 
|  | 506 | # Create it | 
|  | 507 | self.CoreV1.create_namespaced_config_map( | 
|  | 508 | namespace=ns, | 
|  | 509 | body=_cm | 
|  | 510 | ) | 
|  | 511 | logger_cli.debug("... created config map '{}/{}'".format( | 
|  | 512 | ns, | 
|  | 513 | name | 
|  | 514 | )) | 
|  | 515 |  | 
|  | 516 | return _data.keys() | 
|  | 517 |  | 
|  | 518 | def prepare_daemonset_from_yaml(self, ns, ds_yaml): | 
|  | 519 | _name = ds_yaml['metadata']['name'] | 
|  | 520 | _ds = self.get_daemon_set_by_name(ns, _name) | 
|  | 521 |  | 
|  | 522 | if _ds is not None: | 
|  | 523 | logger_cli.debug( | 
|  | 524 | "... found existing daemonset '{}'".format(_name) | 
|  | 525 | ) | 
|  | 526 | _r = self.AppsV1.replace_namespaced_daemon_set( | 
|  | 527 | _ds.metadata.name, | 
|  | 528 | _ds.metadata.namespace, | 
|  | 529 | body=ds_yaml | 
|  | 530 | ) | 
|  | 531 | logger_cli.debug( | 
|  | 532 | "... replacing existing daemonset '{}'".format(_name) | 
|  | 533 | ) | 
|  | 534 | return _r | 
|  | 535 | else: | 
|  | 536 | logger_cli.debug( | 
|  | 537 | "... creating daemonset '{}'".format(_name) | 
|  | 538 | ) | 
|  | 539 | _r = self.AppsV1.create_namespaced_daemon_set(ns, body=ds_yaml) | 
|  | 540 | return _r | 
|  | 541 |  | 
|  | 542 | def delete_daemon_set_by_name(self, ns, name): | 
|  | 543 | return self.AppsV1.delete_namespaced_daemon_set(name, ns) | 
|  | 544 |  | 
|  | 545 | def exec_on_all_pods(self, pods): | 
|  | 546 | """ | 
|  | 547 | Create multiple threads to execute script on all target pods | 
|  | 548 | """ | 
|  | 549 | # Create map for threads: [[node_name, ns, pod_name]...] | 
|  | 550 | _pod_list = [] | 
|  | 551 | for item in pods.items: | 
|  | 552 | _pod_list.append( | 
|  | 553 | [ | 
|  | 554 | item.spec.nodeName, | 
|  | 555 | item.metadata.namespace, | 
|  | 556 | item.metadata.name | 
|  | 557 | ] | 
|  | 558 | ) | 
|  | 559 |  | 
|  | 560 | # map func and cmd | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 561 | logger_cli.error("ERROR: 'exec_on_all_pods'is not implemented yet") | 
| Alex | 1f90e7b | 2021-09-03 15:31:28 -0500 | [diff] [blame] | 562 | # create result list | 
|  | 563 |  | 
|  | 564 | return [] | 
| Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 565 |  | 
|  | 566 | @retry(ApiException) | 
|  | 567 | def get_pods_for_daemonset(self, ds): | 
|  | 568 | # get all pod names for daemonset | 
|  | 569 | logger_cli.debug( | 
|  | 570 | "... extracting pod names from daemonset '{}'".format( | 
|  | 571 | ds.metadata.name | 
|  | 572 | ) | 
|  | 573 | ) | 
|  | 574 | _ns = ds.metadata.namespace | 
|  | 575 | _name = ds.metadata.name | 
|  | 576 | _pods = self.CoreV1.list_namespaced_pod( | 
|  | 577 | namespace=_ns, | 
|  | 578 | label_selector='name={}'.format(_name) | 
|  | 579 | ) | 
|  | 580 | return _pods | 
|  | 581 |  | 
|  | 582 | def put_string_buffer_to_pod_as_textfile( | 
|  | 583 | self, | 
|  | 584 | pod_name, | 
|  | 585 | namespace, | 
|  | 586 | buffer, | 
|  | 587 | filepath, | 
|  | 588 | _request_timeout=120, | 
|  | 589 | **kwargs | 
|  | 590 | ): | 
|  | 591 | _command = ['/bin/sh'] | 
|  | 592 | response = stream( | 
|  | 593 | self.CoreV1.connect_get_namespaced_pod_exec, | 
|  | 594 | pod_name, | 
|  | 595 | namespace, | 
|  | 596 | command=_command, | 
|  | 597 | stderr=True, | 
|  | 598 | stdin=True, | 
|  | 599 | stdout=True, | 
|  | 600 | tty=False, | 
|  | 601 | _request_timeout=_request_timeout, | 
|  | 602 | _preload_content=False, | 
|  | 603 | **kwargs | 
|  | 604 | ) | 
|  | 605 |  | 
|  | 606 | # if json | 
|  | 607 | # buffer = json.dumps(_dict, indent=2).encode('utf-8') | 
|  | 608 |  | 
|  | 609 | commands = [ | 
|  | 610 | bytes("cat <<'EOF' >" + filepath + "\n", 'utf-8'), | 
|  | 611 | buffer, | 
|  | 612 | bytes("\n" + "EOF\n", 'utf-8') | 
|  | 613 | ] | 
|  | 614 |  | 
|  | 615 | while response.is_open(): | 
|  | 616 | response.update(timeout=1) | 
|  | 617 | if response.peek_stdout(): | 
|  | 618 | logger_cli.debug("... STDOUT: %s" % response.read_stdout()) | 
|  | 619 | if response.peek_stderr(): | 
|  | 620 | logger_cli.debug("... STDERR: %s" % response.read_stderr()) | 
|  | 621 | if commands: | 
|  | 622 | c = commands.pop(0) | 
|  | 623 | logger_cli.debug("... running command... {}\n".format(c)) | 
|  | 624 | response.write_stdin(str(c, encoding='utf-8')) | 
|  | 625 | else: | 
|  | 626 | break | 
|  | 627 | response.close() | 
|  | 628 |  | 
|  | 629 | # Force recreate of Api objects | 
|  | 630 | self._coreV1 = None | 
|  | 631 |  | 
|  | 632 | return | 
| Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 633 |  | 
|  | 634 | def get_custom_resource(self, group, version, plural): | 
|  | 635 | # Get it | 
|  | 636 | # Example: | 
|  | 637 | # kubernetes.client.CustomObjectsApi().list_cluster_custom_object( | 
|  | 638 | #   group="networking.istio.io", | 
|  | 639 | #   version="v1alpha3", | 
|  | 640 | #   plural="serviceentries" | 
|  | 641 | # ) | 
|  | 642 | return self.CustomObjects.list_cluster_custom_object( | 
|  | 643 | group=group, | 
|  | 644 | version=version, | 
|  | 645 | plural=plural | 
|  | 646 | ) |