blob: 7ca518edeed58a86778c86b0ec95cdd346d7fc7f [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
Victor Ryzhenkin8ff3c3f2018-01-17 19:37:05 +040015import os
Vladimir Jigulina6b018b2018-07-18 15:19:01 +040016import requests
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +040017import yaml
Artem Panchenko0594cd72017-06-12 13:25:26 +030018
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
Artem Panchenko0594cd72017-06-12 13:25:26 +030027
28LOG = logger.logger
29
30
31class K8SManager(ExecuteCommandsMixin):
32 """docstring for K8SManager"""
33
34 __config = None
35 __underlay = None
36
37 def __init__(self, config, underlay, salt):
38 self.__config = config
39 self.__underlay = underlay
40 self._salt = salt
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +040041 self._api = None
42 self.kubectl = K8SKubectlCli(self)
43 self.virtlet = K8SVirtlet(self)
44 super(K8SManager, self).__init__(config=config, underlay=underlay)
Artem Panchenko0594cd72017-06-12 13:25:26 +030045
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
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +040075 def _api_init(self):
76 ca_result = self.controller_check_call(
77 'base64 --wrap=0 /etc/kubernetes/ssl/ca-kubernetes.crt')
78
79 self._api = cluster.K8sCluster(
80 user=self.__config.k8s_deploy.kubernetes_admin_user,
81 password=self.__config.k8s_deploy.kubernetes_admin_password,
82 ca=ca_result['stdout'][0],
83 host=self.__config.k8s.kube_host,
84 port=self.__config.k8s.kube_apiserver_port)
85
Artem Panchenko0594cd72017-06-12 13:25:26 +030086 @property
87 def api(self):
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +040088 """
89 :rtype: cluster.K8sCluster
90 """
91 if self._api is None:
92 self._api_init()
93 return self._api
Artem Panchenko0594cd72017-06-12 13:25:26 +030094
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +040095 def get_controllers(self):
96 """ Return list of controllers ssh underlays """
Vladimir Jigulin34dfa942018-07-23 21:05:48 +040097 return [node for node in self.__config.underlay.ssh if
98 ext.UNDERLAY_NODE_ROLES.k8s_controller in node['roles']]
99
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400100 def get_masters(self):
101 """ Return list of kubernetes masters hosts fqdn """
102 masters_fqdn = self._salt.get_pillar(
103 tgt='I@kubernetes:master', pillar='linux:network:fqdn')
104 return [self.__underlay.host_by_node_name(node_name=v)
105 for pillar in masters_fqdn for k, v in pillar.items()]
106
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400107 @property
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400108 def controller_name(self):
109 """ Return node name of controller node that used for all actions """
110 names = [node['node_name'] for node in self.get_controllers()]
111 # we want to return same controller name every time
112 names.sort()
113 return names[0]
Victor Ryzhenkin66d39372017-09-28 19:25:48 +0400114
Dennis Dmitriev66650fc2018-11-02 11:04:37 +0200115 @property
116 def controller_minion_id(self):
117 """ Return node name of controller node that used for all actions """
118 minion_ids = [minion_id['minion_id'] for minion_id in
119 self.get_controllers()]
120 # we want to return same controller name every time
121 minion_ids.sort()
122 return minion_ids[0]
123
124 @property
125 def is_metallb_enabled(self):
126 ctl_tgt = self.controller_minion_id
127 LOG.debug("Controller target: {}".format(ctl_tgt))
128
129 result = self._salt.get_pillar(
130 tgt=ctl_tgt,
131 pillar='kubernetes:common:addons:metallb:enabled')
132 metallb = result[0].get(ctl_tgt, False)
133 LOG.info("{} kubernetes:common:addons:metallb:enabled: {}"
134 .format(ctl_tgt, bool(metallb)))
135 return metallb
136
137 @property
138 def is_ingress_nginx_enabled(self):
139 ctl_tgt = self.controller_minion_id
140 LOG.debug("Controller target: {}".format(ctl_tgt))
141
142 result = self._salt.get_pillar(
143 tgt=ctl_tgt,
144 pillar='kubernetes:common:addons:ingress-nginx:enabled')
145 ingress_nginx = result[0].get(ctl_tgt, False)
146 LOG.info("{} kubernetes:common:addons:ingress-nginx:enabled: {}"
147 .format(ctl_tgt, bool(ingress_nginx)))
148 return ingress_nginx
149
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400150 def controller_check_call(self, cmd, **kwargs):
151 """ Run command on controller and return result """
152 LOG.info("running cmd on k8s controller: {}".format(cmd))
Vladimir Jigulinee1faa52018-06-25 13:00:51 +0400153 return self.__underlay.check_call(
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400154 cmd=cmd, node_name=self.controller_name, **kwargs)
Artem Panchenko501e67e2017-06-14 14:59:18 +0300155
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400156 def get_keepalived_vip(self):
Vladimir Jigulin62bcf462018-05-28 18:17:01 +0400157 """
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400158 Return k8s VIP IP address
Vladimir Jigulin62bcf462018-05-28 18:17:01 +0400159
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400160 :return: str, IP address
Vladimir Jigulin62bcf462018-05-28 18:17:01 +0400161 """
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400162 ctl_vip_pillar = self._salt.get_pillar(
163 tgt="I@kubernetes:control:enabled:True",
164 pillar="_param:cluster_vip_address")[0]
165 return ctl_vip_pillar.values()[0]
Vladimir Jigulin62bcf462018-05-28 18:17:01 +0400166
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400167 def run_sample_deployment(self, name, **kwargs):
168 return K8SSampleDeployment(self, name, **kwargs)
Victor Ryzhenkin3ffa2b42017-10-05 16:38:44 +0400169
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400170 def get_pod_ips_from_container(self, pod_name, exclude_local=True,
171 namespace='default'):
172 """ Get ips from container using 'ip a'
173 Required for cni-genie multi-cni cases
174
175 :return: list of IP adresses
Victor Ryzhenkin3ffa2b42017-10-05 16:38:44 +0400176 """
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400177 cmd = "ip a|grep \"inet \"|awk '{{print $2}}'"
178 result = self.kubectl.cli_exec(namespace, pod_name, cmd)['stdout']
179 ips = [line.strip().split('/')[0] for line in result]
180 if exclude_local:
181 ips = [ip for ip in ips if not ip.startswith("127.")]
182 return ips
Victor Ryzhenkin3ffa2b42017-10-05 16:38:44 +0400183
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400184 def update_k8s_version(self, tag):
Vladimir Jigulin62bcf462018-05-28 18:17:01 +0400185 """
186 Update k8s images tag version in cluster meta and apply required
187 for update states
188
189 :param tag: New version tag of k8s images
190 :return:
191 """
192 master_host = self.__config.salt.salt_master_host
193
194 def update_image_tag_meta(config, image_name):
195 image_old = config.get(image_name)
196 image_base = image_old.split(':')[0]
197 image_new = "{}:{}".format(image_base, tag)
198 LOG.info("Changing k8s '{0}' image cluster meta to '{1}'".format(
199 image_name, image_new))
200
201 with self.__underlay.remote(host=master_host) as r:
202 cmd = "salt-call reclass.cluster_meta_set" \
203 " name={0} value={1}".format(image_name, image_new)
204 r.check_call(cmd)
205 return image_new
206
207 cfg = self.__config
208
209 update_image_tag_meta(cfg.k8s_deploy, "kubernetes_hyperkube_image")
210 update_image_tag_meta(cfg.k8s_deploy, "kubernetes_pause_image")
211 cfg.k8s.k8s_conformance_image = update_image_tag_meta(
212 cfg.k8s, "k8s_conformance_image")
213
214 steps_path = cfg.k8s_deploy.k8s_update_steps_path
215 update_commands = self.__underlay.read_template(steps_path)
216 self.execute_commands(
217 update_commands, label="Updating kubernetes to '{}'".format(tag))
Vladimir Jigulinee1faa52018-06-25 13:00:51 +0400218
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400219 def run_conformance(self, timeout=60*60, log_out='k8s_conformance.log',
220 raise_on_err=True, node_name=None,
221 api_server='http://127.0.0.1:8080'):
222 if node_name is None:
223 node_name = self.controller_name
224 cmd = "set -o pipefail; docker run --net=host " \
225 "-e API_SERVER='{api}' {image} | tee '{log}'".format(
226 api=api_server, log=log_out,
227 image=self.__config.k8s.k8s_conformance_image)
228 return self.__underlay.check_call(
229 cmd=cmd, node_name=node_name, timeout=timeout,
Dennis Dmitrieveb50ce12018-09-27 13:34:32 +0300230 raise_on_err=raise_on_err, verbose=True)
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400231
232 def run_virtlet_conformance(self, timeout=60 * 120,
233 log_file='virtlet_conformance.log'):
234 if self.__config.k8s.run_extended_virtlet_conformance:
235 ci_image = "cloud-images.ubuntu.com/xenial/current/" \
236 "xenial-server-cloudimg-amd64-disk1.img"
237 cmd = ("set -o pipefail; "
238 "docker run --net=host {0} /virtlet-e2e-tests "
239 "-include-cloud-init-tests -junitOutput report.xml "
240 "-image {2} -sshuser ubuntu -memoryLimit 1024 "
241 "-alsologtostderr -cluster-url http://127.0.0.1:8080 "
242 "-ginkgo.focus '\[Conformance\]' "
243 "| tee {1}".format(
244 self.__config.k8s_deploy.kubernetes_virtlet_image,
245 log_file, ci_image))
246 else:
247 cmd = ("set -o pipefail; "
248 "docker run --net=host {0} /virtlet-e2e-tests "
249 "-junitOutput report.xml "
250 "-alsologtostderr -cluster-url http://127.0.0.1:8080 "
251 "-ginkgo.focus '\[Conformance\]' "
252 "| tee {1}".format(
253 self.__config.k8s_deploy.kubernetes_virtlet_image,
254 log_file))
255 LOG.info("Executing: {}".format(cmd))
256 with self.__underlay.remote(
257 node_name=self.controller_name) as remote:
258 result = remote.check_call(cmd, timeout=timeout)
259 stderr = result['stderr']
260 stdout = result['stdout']
261 LOG.info("Test results stdout: {}".format(stdout))
262 LOG.info("Test results stderr: {}".format(stderr))
263 return result
264
265 def start_k8s_cncf_verification(self, timeout=60 * 90):
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +0400266 """
267 Build sonobuoy using golang docker image and install it in system
268 Then generate sonobuoy verification manifest using gen command
269 and wait for it to end using status annotation
270 TODO (vjigulin): All sonobuoy methods can be improved if we define
271 correct KUBECONFIG env variable
272 """
273 cncf_cmd =\
274 'docker run --network host --name sonobuoy golang:1.11 bash -c ' \
275 '"go get -d -v github.com/heptio/sonobuoy && ' \
276 'go install -v github.com/heptio/sonobuoy" && ' \
277 'docker cp sonobuoy:/go/bin/sonobuoy /usr/local/bin/ && ' \
278 'docker rm sonobuoy && ' \
279 'sonobuoy gen | kubectl apply -f -'
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400280
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +0400281 self.controller_check_call(cncf_cmd, timeout=900)
282
283 sonobuoy_pod = self.api.pods.get('sonobuoy', 'heptio-sonobuoy')
284 sonobuoy_pod.wait_running()
285
286 def sonobuoy_status():
287 annotations = sonobuoy_pod.read().metadata.annotations
288 json_status = annotations['sonobuoy.hept.io/status']
289 status = yaml.safe_load(json_status)['status']
290 if status != 'running':
291 LOG.info("CNCF status: {}".format(json_status))
292 return yaml.safe_load(json_status)['status']
293
294 LOG.info("Waiting for CNCF to complete")
295 helpers.wait(
296 lambda: sonobuoy_status() == 'complete',
297 interval=30, timeout=timeout,
298 timeout_msg="Timeout for CNCF reached."
299 )
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400300
301 def extract_file_to_node(self, system='docker',
302 container='virtlet',
303 file_path='report.xml',
304 out_dir='.',
305 **kwargs):
Vladimir Jigulinee1faa52018-06-25 13:00:51 +0400306 """
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400307 Download file from docker or k8s container to node
Vladimir Jigulinee1faa52018-06-25 13:00:51 +0400308
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400309 :param system: docker or k8s
310 :param container: Full name of part of name
311 :param file_path: File path in container
312 :param kwargs: Used to control pod and namespace
313 :param out_dir: Output directory
314 :return:
Vladimir Jigulinee1faa52018-06-25 13:00:51 +0400315 """
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +0400316 with self.__underlay.remote(node_name=self.controller_name) as remote:
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400317 if system is 'docker':
318 cmd = ("docker ps --all | grep \"{0}\" |"
319 " awk '{{print $1}}'".format(container))
320 result = remote.check_call(cmd, raise_on_err=False)
321 if result['stdout']:
322 container_id = result['stdout'][0].strip()
323 else:
324 LOG.info('No container found, skipping extraction...')
325 return
326 cmd = "docker start {}".format(container_id)
327 remote.check_call(cmd, raise_on_err=False)
328 cmd = "docker cp \"{0}:/{1}\" \"{2}\"".format(
329 container_id, file_path, out_dir)
330 remote.check_call(cmd, raise_on_err=False)
331 else:
332 # system is k8s
333 pod_name = kwargs.get('pod_name')
334 pod_namespace = kwargs.get('pod_namespace')
335 cmd = 'kubectl cp \"{0}/{1}:/{2}\" \"{3}\"'.format(
336 pod_namespace, pod_name, file_path, out_dir)
337 remote.check_call(cmd, raise_on_err=False)
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400338
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400339 def download_k8s_logs(self, files):
340 """
341 Download JUnit report and conformance logs from cluster
342 :param files:
343 :return:
344 """
345 master_host = self.__config.salt.salt_master_host
346 with self.__underlay.remote(host=master_host) as r:
347 for log_file in files:
348 cmd = "rsync -r \"{0}:/root/{1}\" /root/".format(
349 self.controller_name, log_file)
350 r.check_call(cmd, raise_on_err=False)
351 LOG.info("Downloading the artifact {0}".format(log_file))
352 r.download(destination=log_file, target=os.getcwd())
Dennis Dmitriev445b4322018-10-08 14:32:07 +0300353 self.store_server_version(os.path.join(os.getcwd(), 'env_k8s_version'))
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400354
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400355 def combine_xunit(self, path, output):
356 """
357 Function to combine multiple xmls with test results to
358 one.
Vladimir Jigulin34dfa942018-07-23 21:05:48 +0400359
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400360 :param path: Path where xmls to combine located
361 :param output: Path to xml file where output will stored
362 :return:
363 """
364 with self.__underlay.remote(node_name=self.controller_name) as r:
365 cmd = ("apt-get install python-setuptools -y; "
366 "pip install "
367 "https://github.com/mogaika/xunitmerge/archive/master.zip")
368 LOG.debug('Installing xunitmerge')
369 r.check_call(cmd, raise_on_err=False)
370 LOG.debug('Merging xunit')
Dennis Dmitrievee5ef232018-08-31 13:53:18 +0300371 cmd = ("cd {0}; arg=''; "
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400372 "for i in $(ls | grep xml); "
373 "do arg=\"$arg $i\"; done && "
374 "xunitmerge $arg {1}".format(path, output))
375 r.check_call(cmd, raise_on_err=False)
Vladimir Jigulin34dfa942018-07-23 21:05:48 +0400376
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400377 def manage_cncf_archive(self):
378 """
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +0400379 Function to untar archive, move files that we are needs to the
380 home folder, prepare it to downloading.
381 Will generate files on controller node:
382 e2e.log, junit_01.xml, cncf_results.tar.gz, version.txt
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400383 :return:
384 """
385
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +0400386 cmd =\
387 "rm -rf cncf_results.tar.gz result && " \
388 "mkdir result && " \
389 "mv *_sonobuoy_*.tar.gz cncf_results.tar.gz && " \
390 "tar -C result -xzf cncf_results.tar.gz && " \
391 "mv result/plugins/e2e/results/e2e.log . ; " \
392 "mv result/plugins/e2e/results/junit_01.xml . ; " \
393 "kubectl version > version.txt"
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400394
Vladimir Jigulin0c8dd5a2018-08-28 05:08:35 +0400395 self.controller_check_call(cmd, raise_on_err=False)
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400396
397 @retry(300, exception=DevopsCalledProcessError)
398 def nslookup(self, host, src):
399 """ Run nslookup on controller and return result """
400 return self.controller_check_call("nslookup {0} {1}".format(host, src))
401
402 @retry(300, exception=DevopsCalledProcessError)
Vladimir Jigulin57ecae92018-09-10 22:51:15 +0400403 def curl(self, url, *args):
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400404 """
405 Run curl on controller and return stdout
406
407 :param url: url to curl
Vladimir Jigulin57ecae92018-09-10 22:51:15 +0400408 :return: list of strings (with /n at end of every line)
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400409 """
Vladimir Jigulin57ecae92018-09-10 22:51:15 +0400410 args = list(args)
411 args.append(url)
412 cmd = "curl -s -S {}".format(
413 " ".join(["'{}'".format(a.replace("'", "\\'")) for a in args]))
414 result = self.controller_check_call(cmd)
415 LOG.debug("{0}\nresult:\n{1}".format(cmd, result['stdout']))
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400416 return result['stdout']
Vladimir Jigulin34dfa942018-07-23 21:05:48 +0400417
Dennis Dmitriev445b4322018-10-08 14:32:07 +0300418 def store_server_version(self, env_file_path):
419 """Store Kubernetes server version in bash source file"""
420
421 def digits(string):
422 return ''.join(n for n in string if n.isdigit())
423
424 ver = self.api.api_version.get_code()
425 LOG.debug("Got Kubernetes server version:\n{0}".format(ver))
426
427 env_version = ("export KUBE_SERVER_VERSION={0}.{1}\n"
428 "export KUBE_SERVER_GIT_VERSION={2}\n"
429 .format(digits(ver.major),
430 digits(ver.minor),
431 ver.git_version))
432
433 LOG.info("Kubernetes server version is stored to {0}:\n{1}"
434 .format(env_file_path, env_version))
435 with open(env_file_path, 'w') as kver:
436 kver.write(env_version)
437
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400438
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400439class K8SKubectlCli(object):
440 """ Contain kubectl cli commands and api wrappers"""
441 def __init__(self, manager):
442 self._manager = manager
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400443
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400444 def cli_run(self, namespace, name, image, port, replicas=1):
445 cmd = "kubectl -n {0} run {1} --image={2} --port={3} --replicas={4}".\
446 format(namespace, name, image, port, replicas)
447 return self._manager.controller_check_call(cmd)
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400448
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400449 def run(self, namespace, name, image, port, replicas=1):
450 self.cli_run(namespace, name, image, port, replicas)
451 return self._manager.api.deployments.get(
452 namespace=namespace, name=name)
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400453
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400454 def cli_expose(self, namespace, resource_type, resource_name,
455 service_name=None, port='', service_type='ClusterIP'):
456 cmd = "kubectl -n {0} expose {1} {2} --port={3} --type={4}".format(
457 namespace, resource_type, resource_name, port, service_type)
458 if service_name is not None:
459 cmd += " --name={}".format(service_name)
460 return self._manager.controller_check_call(cmd)
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400461
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400462 def expose(self, resource, service_name=None,
463 port='', service_type='ClusterIP'):
464 self.cli_expose(resource.namespace, resource.resource_type,
465 resource.name, service_name=service_name,
466 port=port, service_type=service_type)
467 return self._manager.api.services.get(
468 namespace=resource.namespace, name=service_name or resource.name)
469
470 def cli_exec(self, namespace, pod_name, cmd, container=''):
471 kubectl_cmd = "kubectl -n {0} exec --container={1} {2} -- {3}".format(
472 namespace, container, pod_name, cmd)
473 return self._manager.controller_check_call(kubectl_cmd)
474
475 # def exec(...), except exec is statement in python
476 def execute(self, pod, cmd, container=''):
477 return self.cli_exec(pod.namespace, pod.name, cmd, container=container)
478
479 def cli_annotate(self, namespace, resource_type, resource_name,
480 annotations, overwrite=False):
481 cmd = "kubectl -n {0} annotate {1} {2} {3}".format(
482 namespace, resource_type, resource_name, annotations)
483 if overwrite:
484 cmd += " --overwrite"
485 return self._manager.controller_check_call(cmd)
486
487 def annotate(self, resource, annotations, overwrite=False):
488 return self.cli_annotate(resource.namespace, resource.resource_type,
489 resource.name, annotations,
490 overwrite=overwrite)
491
492
493class K8SVirtlet(object):
494 """ Contain virtlet-related methods"""
495 def __init__(self, manager, namespace='kube-system'):
496 self._manager = manager
497 self._namespace = namespace
498
499 def get_virtlet_node_pod(self, node_name):
500 for pod in self._manager.api.pods.list(
501 namespace=self._namespace, name_prefix='virtlet-'):
502 if pod.read().spec.node_name == node_name:
503 return pod
504 return None
505
506 def get_pod_dom_uuid(self, pod):
507 uuid_name_map = self.virtlet_execute(
508 pod.read().spec.node_name, 'virsh list --uuid --name')['stdout']
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400509 for line in uuid_name_map:
510 if line.rstrip().endswith("-{}".format(pod.name)):
511 return line.split(" ")[0]
512 raise Exception("Cannot detect uuid for pod {}".format(pod.name))
513
514 def virsh_domstats(self, pod):
515 """ get dict of vm stats """
516 uuid = self.get_pod_dom_uuid(pod)
517 result = self.virtlet_execute(
518 pod.read().spec.node_name, 'virsh domstats {}'.format(uuid))
519 stats = dict()
520 for line in result['stdout']:
521 if '=' in line:
522 vals = line.strip().split('=')
523 stats[vals[0]] = vals[1]
524 return stats
525
526 def virtlet_execute(self, node_name, cmd, container='libvirt'):
527 """ execute command inside virtlet container """
528 pod = self.get_virtlet_node_pod(node_name)
529 return self._manager.kubectl.execute(pod, cmd, container)
530
531
532class K8SSampleDeployment(object):
533 """ Wrapper for deployment run=>expose=>check frequent combination """
534 def __init__(self, manager, name,
535 namespace=None,
536 image='gcr.io/google-samples/node-hello:1.0',
537 port=8080,
538 replicas=2):
539 namespace = namespace or manager.api.default_namespace
540
541 self._manager = manager
542 self._port = port
543 self._deployment = \
544 manager.kubectl.run(namespace, name,
545 image=image, port=port, replicas=replicas)
546 self._index = 1 # used to generate svc name
547 self._svc = None # hold last created svc
548
549 def wait_ready(self, timeout=300, interval=5):
550 self._deployment.wait_ready(timeout=timeout, interval=interval)
551 return self
552
553 def svc(self):
554 """ Return the last exposed service"""
555 return self._svc
556
557 def expose(self, service_type='ClusterIP'):
558 service_name = "{0}-s{1}".format(self._deployment.name, self._index)
Vladimir Jigulin90689152018-09-26 15:38:19 +0400559 self._index += 1
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400560 self._svc = self._manager.kubectl.expose(
561 self._deployment, port=self._port,
562 service_name=service_name, service_type=service_type)
563 return self._svc
564
565 def curl(self, svc=None, external=False):
566 if svc is None:
567 svc = self.svc()
568 url = "http://{0}:{1}".format(svc.get_ip(external), self._port)
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400569 if external:
570 return requests.get(url).text
571 else:
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400572 return self._manager.curl(url)
Vladimir Jigulina6b018b2018-07-18 15:19:01 +0400573
Vladimir Jigulin4ad52a82018-08-12 05:51:30 +0400574 def is_service_available(self, svc=None, external=False):
575 return "Hello Kubernetes!" in self.curl(svc, external=external)
Vladimir Jigulin90689152018-09-26 15:38:19 +0400576
577 def delete(self):
578 for svc in self._manager.api.services.list_all(
579 name_prefix="{}-s".format(self._deployment.name)):
580 svc.delete()
581 self._deployment.delete()