blob: 87e3eafb80c6186ec44a23a9a8f4eabbff873096 [file] [log] [blame]
Artem Panchenko0594cd72017-06-12 13:25:26 +03001# Copyright 2017 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import time
16
17import yaml
18
19from devops.helpers import helpers
Victor Ryzhenkin66d39372017-09-28 19:25:48 +040020from devops.error import DevopsCalledProcessError
Artem Panchenko0594cd72017-06-12 13:25:26 +030021
22from tcp_tests import logger
Victor Ryzhenkin66d39372017-09-28 19:25:48 +040023from tcp_tests.helpers import ext
24from tcp_tests.helpers.utils import retry
Artem Panchenko0594cd72017-06-12 13:25:26 +030025from tcp_tests.managers.execute_commands import ExecuteCommandsMixin
26from tcp_tests.managers.k8s import cluster
Sergey Vasilenkofd1fd612017-09-20 13:09:51 +030027from k8sclient.client.rest import ApiException
Artem Panchenko0594cd72017-06-12 13:25:26 +030028
29LOG = logger.logger
30
31
32class K8SManager(ExecuteCommandsMixin):
33 """docstring for K8SManager"""
34
35 __config = None
36 __underlay = None
37
38 def __init__(self, config, underlay, salt):
39 self.__config = config
40 self.__underlay = underlay
41 self._salt = salt
42 self._api_client = None
43 super(K8SManager, self).__init__(
44 config=config, underlay=underlay)
45
46 def install(self, commands):
47 self.execute_commands(commands,
48 label='Install Kubernetes services')
49 self.__config.k8s.k8s_installed = True
50 self.__config.k8s.kube_host = self.get_proxy_api()
51
52 def get_proxy_api(self):
53 k8s_proxy_ip_pillars = self._salt.get_pillar(
vrovachev99228d32017-06-08 19:46:10 +040054 tgt='I@haproxy:proxy:enabled:true and I@kubernetes:master',
Artem Panchenko0594cd72017-06-12 13:25:26 +030055 pillar='haproxy:proxy:listen:k8s_secure:binds:address')
vrovachev99228d32017-06-08 19:46:10 +040056 k8s_hosts = self._salt.get_pillar(
57 tgt='I@haproxy:proxy:enabled:true and I@kubernetes:master',
58 pillar='kubernetes:pool:apiserver:host')
Artem Panchenko0594cd72017-06-12 13:25:26 +030059 k8s_proxy_ip = set([ip
60 for item in k8s_proxy_ip_pillars
Dina Belovae6fdffb2017-09-19 13:58:34 -070061 for node, ip in item.items() if ip])
vrovachev99228d32017-06-08 19:46:10 +040062 k8s_hosts = set([ip
Dina Belovae6fdffb2017-09-19 13:58:34 -070063 for item in k8s_hosts
64 for node, ip in item.items() if ip])
vrovachev99228d32017-06-08 19:46:10 +040065 assert len(k8s_hosts) == 1, (
66 "Found more than one Kubernetes API hosts in pillars:{0}, "
67 "expected one!").format(k8s_hosts)
68 k8s_host = k8s_hosts.pop()
69 assert k8s_host in k8s_proxy_ip, (
70 "Kubernetes API host:{0} not found in proxies:{} "
71 "on k8s master nodes. K8s proxies are expected on "
72 "nodes with K8s master").format(k8s_host, k8s_proxy_ip)
73 return k8s_host
Artem Panchenko0594cd72017-06-12 13:25:26 +030074
75 @property
76 def api(self):
77 if self._api_client is None:
78 self._api_client = cluster.K8sCluster(
79 user=self.__config.k8s_deploy.kubernetes_admin_user,
80 password=self.__config.k8s_deploy.kubernetes_admin_password,
81 host=self.__config.k8s.kube_host,
82 port=self.__config.k8s.kube_apiserver_port,
83 default_namespace='default')
84 return self._api_client
85
Victor Ryzhenkin66d39372017-09-28 19:25:48 +040086 @property
87 def ctl_host(self):
88 nodes = [node for node in self.__config.underlay.ssh if
89 ext.UNDERLAY_NODE_ROLES.k8s_controller in node['roles']]
90 return nodes[0]['node_name']
91
Artem Panchenko0594cd72017-06-12 13:25:26 +030092 def get_pod_phase(self, pod_name, namespace=None):
93 return self.api.pods.get(
94 name=pod_name, namespace=namespace).phase
95
96 def wait_pod_phase(self, pod_name, phase, namespace=None, timeout=60):
97 """Wait phase of pod_name from namespace while timeout
98
99 :param str: pod_name
100 :param str: namespace
101 :param list or str: phase
102 :param int: timeout
103
104 :rtype: None
105 """
106 if isinstance(phase, str):
107 phase = [phase]
108
109 def check():
110 return self.get_pod_phase(pod_name, namespace) in phase
111
112 helpers.wait(check, timeout=timeout,
113 timeout_msg='Timeout waiting, pod {pod_name} is not in '
114 '"{phase}" phase'.format(
115 pod_name=pod_name, phase=phase))
116
117 def wait_pods_phase(self, pods, phase, timeout=60):
118 """Wait timeout seconds for phase of pods
119
120 :param pods: list of K8sPod
121 :param phase: list or str
122 :param timeout: int
123
124 :rtype: None
125 """
126 if isinstance(phase, str):
127 phase = [phase]
128
129 def check(pod_name, namespace):
130 return self.get_pod_phase(pod_name, namespace) in phase
131
132 def check_all_pods():
133 return all(check(pod.name, pod.metadata.namespace) for pod in pods)
134
135 helpers.wait(
136 check_all_pods,
137 timeout=timeout,
138 timeout_msg='Timeout waiting, pods {0} are not in "{1}" '
139 'phase'.format([pod.name for pod in pods], phase))
140
141 def check_pod_create(self, body, namespace=None, timeout=300, interval=5):
142 """Check creating sample pod
143
144 :param k8s_pod: V1Pod
145 :param namespace: str
146 :rtype: V1Pod
147 """
148 LOG.info("Creating pod in k8s cluster")
149 LOG.debug(
150 "POD spec to create:\n{}".format(
151 yaml.dump(body, default_flow_style=False))
152 )
153 LOG.debug("Timeout for creation is set to {}".format(timeout))
154 LOG.debug("Checking interval is set to {}".format(interval))
155 pod = self.api.pods.create(body=body, namespace=namespace)
156 pod.wait_running(timeout=300, interval=5)
157 LOG.info("Pod '{0}' is created in '{1}' namespace".format(
158 pod.name, pod.namespace))
159 return self.api.pods.get(name=pod.name, namespace=pod.namespace)
160
161 def wait_pod_deleted(self, podname, timeout=60, interval=5):
162 helpers.wait(
163 lambda: podname not in [pod.name for pod in self.api.pods.list()],
164 timeout=timeout,
165 interval=interval,
166 timeout_msg="Pod deletion timeout reached!"
167 )
168
169 def check_pod_delete(self, k8s_pod, timeout=300, interval=5,
170 namespace=None):
171 """Deleting pod from k8s
172
173 :param k8s_pod: tcp_tests.managers.k8s.nodes.K8sNode
174 :param k8sclient: tcp_tests.managers.k8s.cluster.K8sCluster
175 """
176 LOG.info("Deleting pod '{}'".format(k8s_pod.name))
177 LOG.debug("Pod status:\n{}".format(k8s_pod.status))
178 LOG.debug("Timeout for deletion is set to {}".format(timeout))
179 LOG.debug("Checking interval is set to {}".format(interval))
180 self.api.pods.delete(body=k8s_pod, name=k8s_pod.name,
181 namespace=namespace)
182 self.wait_pod_deleted(k8s_pod.name, timeout, interval)
183 LOG.debug("Pod '{}' is deleted".format(k8s_pod.name))
184
185 def check_service_create(self, body, namespace=None):
186 """Check creating k8s service
187
188 :param body: dict, service spec
189 :param namespace: str
190 :rtype: K8sService object
191 """
192 LOG.info("Creating service in k8s cluster")
193 LOG.debug(
194 "Service spec to create:\n{}".format(
195 yaml.dump(body, default_flow_style=False))
196 )
197 service = self.api.services.create(body=body, namespace=namespace)
198 LOG.info("Service '{0}' is created in '{1}' namespace".format(
199 service.name, service.namespace))
200 return self.api.services.get(name=service.name,
201 namespace=service.namespace)
202
203 def check_ds_create(self, body, namespace=None):
204 """Check creating k8s DaemonSet
205
206 :param body: dict, DaemonSet spec
207 :param namespace: str
208 :rtype: K8sDaemonSet object
209 """
210 LOG.info("Creating DaemonSet in k8s cluster")
211 LOG.debug(
212 "DaemonSet spec to create:\n{}".format(
213 yaml.dump(body, default_flow_style=False))
214 )
215 ds = self.api.daemonsets.create(body=body, namespace=namespace)
216 LOG.info("DaemonSet '{0}' is created in '{1}' namespace".format(
217 ds.name, ds.namespace))
218 return self.api.daemonsets.get(name=ds.name, namespace=ds.namespace)
219
220 def check_ds_ready(self, dsname, namespace=None):
221 """Check if k8s DaemonSet is ready
222
223 :param dsname: str, ds name
224 :return: bool
225 """
226 ds = self.api.daemonsets.get(name=dsname, namespace=namespace)
227 return (ds.status.current_number_scheduled ==
228 ds.status.desired_number_scheduled)
229
230 def wait_ds_ready(self, dsname, namespace=None, timeout=60, interval=5):
231 """Wait until all pods are scheduled on nodes
232
233 :param dsname: str, ds name
234 :param timeout: int
235 :param interval: int
236 """
237 helpers.wait(
238 lambda: self.check_ds_ready(dsname, namespace=namespace),
239 timeout=timeout, interval=interval)
240
Artem Panchenko501e67e2017-06-14 14:59:18 +0300241 def check_deploy_create(self, body, namespace=None):
242 """Check creating k8s Deployment
243
244 :param body: dict, Deployment spec
245 :param namespace: str
246 :rtype: K8sDeployment object
247 """
248 LOG.info("Creating Deployment in k8s cluster")
249 LOG.debug(
250 "Deployment spec to create:\n{}".format(
251 yaml.dump(body, default_flow_style=False))
252 )
253 deploy = self.api.deployments.create(body=body, namespace=namespace)
254 LOG.info("Deployment '{0}' is created in '{1}' namespace".format(
255 deploy.name, deploy.namespace))
256 return self.api.deployments.get(name=deploy.name,
257 namespace=deploy.namespace)
258
259 def check_deploy_ready(self, deploy_name, namespace=None):
260 """Check if k8s Deployment is ready
261
262 :param deploy_name: str, deploy name
263 :return: bool
264 """
Dina Belovae6fdffb2017-09-19 13:58:34 -0700265 deploy = self.api.deployments.get(name=deploy_name,
266 namespace=namespace)
Artem Panchenko501e67e2017-06-14 14:59:18 +0300267 return deploy.status.available_replicas == deploy.status.replicas
268
Dina Belovae6fdffb2017-09-19 13:58:34 -0700269 def wait_deploy_ready(self, deploy_name, namespace=None, timeout=60,
270 interval=5):
Artem Panchenko501e67e2017-06-14 14:59:18 +0300271 """Wait until all pods are scheduled on nodes
272
273 :param deploy_name: str, deploy name
274 :param timeout: int
275 :param interval: int
276 """
277 helpers.wait(
278 lambda: self.check_deploy_ready(deploy_name, namespace=namespace),
279 timeout=timeout, interval=interval)
280
Artem Panchenko0594cd72017-06-12 13:25:26 +0300281 def check_namespace_create(self, name):
282 """Check creating k8s Namespace
283
284 :param name: str
285 :rtype: K8sNamespace object
286 """
Sergey Vasilenkofd1fd612017-09-20 13:09:51 +0300287 try:
288 ns = self.api.namespaces.get(name=name)
289 LOG.info("Namespace '{0}' is already exists".format(ns.name))
290 except ApiException as e:
291 if hasattr(e,"status") and 404 == e.status:
292 LOG.info("Creating Namespace in k8s cluster")
293 ns = self.api.namespaces.create(body={'metadata': {'name': name}})
294 LOG.info("Namespace '{0}' is created".format(ns.name))
295 # wait 10 seconds until a token for new service account is created
296 time.sleep(10)
297 ns = self.api.namespaces.get(name=ns.name)
298 else:
299 raise
300 return ns
Artem Panchenko0594cd72017-06-12 13:25:26 +0300301
302 def create_objects(self, path):
303 if isinstance(path, str):
304 path = [path]
305 params = ' '.join(["-f {}".format(p) for p in path])
306 cmd = 'kubectl create {params}'.format(params=params)
307 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400308 node_name=self.ctl_host) as remote:
Artem Panchenko0594cd72017-06-12 13:25:26 +0300309 LOG.info("Running command '{cmd}' on node {node}".format(
310 cmd=cmd,
311 node=remote.hostname)
312 )
313 result = remote.check_call(cmd)
314 LOG.info(result['stdout'])
315
316 def get_running_pods(self, pod_name, namespace=None):
317 pods = [pod for pod in self.api.pods.list(namespace=namespace)
318 if (pod_name in pod.name and pod.status.phase == 'Running')]
319 return pods
320
321 def get_pods_number(self, pod_name, namespace=None):
322 pods = self.get_running_pods(pod_name, namespace)
323 return len(pods)
324
325 def get_running_pods_by_ssh(self, pod_name, namespace=None):
326 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400327 node_name=self.ctl_host) as remote:
Artem Panchenko0594cd72017-06-12 13:25:26 +0300328 result = remote.check_call("kubectl get pods --namespace {} |"
329 " grep {} | awk '{{print $1 \" \""
330 " $3}}'".format(namespace,
331 pod_name))['stdout']
332 running_pods = [data.strip().split()[0] for data in result
333 if data.strip().split()[1] == 'Running']
334 return running_pods
335
336 def get_pods_restarts(self, pod_name, namespace=None):
337 pods = [pod.status.container_statuses[0].restart_count
338 for pod in self.get_running_pods(pod_name, namespace)]
339 return sum(pods)
vrovacheva9d08332017-06-22 20:01:59 +0400340
341 def run_conformance(self, timeout=60 * 60):
342 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400343 node_name=self.ctl_host) as remote:
vrovacheva9d08332017-06-22 20:01:59 +0400344 result = remote.check_call(
345 "docker run --rm --net=host -e API_SERVER="
346 "'http://127.0.0.1:8080' {}".format(
347 self.__config.k8s.k8s_conformance_image),
348 timeout=timeout)['stdout']
349 return result
Artem Panchenko501e67e2017-06-14 14:59:18 +0300350
351 def get_k8s_masters(self):
352 k8s_masters_fqdn = self._salt.get_pillar(tgt='I@kubernetes:master',
353 pillar='linux:network:fqdn')
354 return [self._K8SManager__underlay.host_by_node_name(node_name=v)
355 for pillar in k8s_masters_fqdn for k, v in pillar.items()]
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400356
357 def kubectl_run(self, name, image, port):
358 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400359 node_name=self.ctl_host) as remote:
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400360 result = remote.check_call(
361 "kubectl run {0} --image={1} --port={2}".format(
362 name, image, port
363 )
364 )
365 return result
366
367 def kubectl_expose(self, resource, name, port, type):
368 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400369 node_name=self.ctl_host) as remote:
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400370 result = remote.check_call(
371 "kubectl expose {0} {1} --port={2} --type={3}".format(
372 resource, name, port, type
373 )
374 )
375 return result
376
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400377 def kubectl_annotate(self, resource, name, annotation):
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400378 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400379 node_name=self.ctl_host) as remote:
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400380 result = remote.check_call(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400381 "kubectl annotate {0} {1} {2}".format(
382 resource, name, annotation
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400383 )
384 )
385 return result
386
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400387 def get_svc_ip(self, name, namespace='kube-system'):
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400388 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400389 node_name=self.ctl_host) as remote:
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400390 result = remote.check_call(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400391 "kubectl get svc {0} -n {1} | "
392 "awk '{{print $2}}' | tail -1".format(name, namespace)
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400393 )
394 return result['stdout'][0].strip()
395
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400396 @retry(300, exception=DevopsCalledProcessError)
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400397 def nslookup(self, host, src):
398 with self.__underlay.remote(
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400399 node_name=self.ctl_host) as remote:
Victor Ryzhenkin14354ac2017-09-27 17:42:30 +0400400 remote.check_call("nslookup {0} {1}".format(host, src))
401