388 lines
17 KiB
Diff
388 lines
17 KiB
Diff
|
|
From b714b4272f8c84060a08f4966b87247e054680c6 Mon Sep 17 00:00:00 2001
|
||
|
|
From: Zhu Huankai <zhuhuankai1@huawei.com>
|
||
|
|
Date: Tue, 18 Jan 2022 20:55:42 +0800
|
||
|
|
Subject: [PATCH 06/10] tests:add stand kata testcases
|
||
|
|
|
||
|
|
Add new testcode to test kata container of standvm and move
|
||
|
|
some functions of vfio to utils_coommon.
|
||
|
|
|
||
|
|
Add some new testcases for standvm of isula:
|
||
|
|
1.test start kata container with initrd.
|
||
|
|
2.test start kata container with rootfs.
|
||
|
|
3.test kata container create template and start from template.
|
||
|
|
4.test start kata container in sandbox
|
||
|
|
5.test start kata container with vfio net device
|
||
|
|
6.test start kata container with vfrtio fs
|
||
|
|
|
||
|
|
Signed-off-by: Zhu Huankai <zhuhuankai1@huawei.com>
|
||
|
|
---
|
||
|
|
.../standvm/functional/test_standvm_isula.py | 228 ++++++++++++++++++
|
||
|
|
.../standvm/functional/test_standvm_vfio.py | 34 +--
|
||
|
|
tests/hydropper/utils/utils_common.py | 31 ++-
|
||
|
|
3 files changed, 265 insertions(+), 28 deletions(-)
|
||
|
|
create mode 100644 tests/hydropper/testcases/standvm/functional/test_standvm_isula.py
|
||
|
|
|
||
|
|
diff --git a/tests/hydropper/testcases/standvm/functional/test_standvm_isula.py b/tests/hydropper/testcases/standvm/functional/test_standvm_isula.py
|
||
|
|
new file mode 100644
|
||
|
|
index 0000000..5e01685
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/tests/hydropper/testcases/standvm/functional/test_standvm_isula.py
|
||
|
|
@@ -0,0 +1,228 @@
|
||
|
|
+# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved.
|
||
|
|
+#
|
||
|
|
+# StratoVirt is licensed under Mulan PSL v2.
|
||
|
|
+# You can use this software according to the terms and conditions of the Mulan
|
||
|
|
+# PSL v2.
|
||
|
|
+# You may obtain a copy of Mulan PSL v2 at:
|
||
|
|
+# http:#license.coscl.org.cn/MulanPSL2
|
||
|
|
+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
|
||
|
|
+# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
||
|
|
+# NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||
|
|
+# See the Mulan PSL v2 for more details.
|
||
|
|
+"""Test standvm isula"""
|
||
|
|
+
|
||
|
|
+import os
|
||
|
|
+import logging
|
||
|
|
+import subprocess
|
||
|
|
+import pytest
|
||
|
|
+import utils.utils_common as utils
|
||
|
|
+from utils.utils_logging import TestLog
|
||
|
|
+
|
||
|
|
+LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||
|
|
+logging.basicConfig(filename="/var/log/pytest.log", level=logging.DEBUG, format=LOG_FORMAT)
|
||
|
|
+LOG = TestLog.get_global_log()
|
||
|
|
+SHELL_TIMEOUT = 10
|
||
|
|
+
|
||
|
|
+def test_standvm_isula_initrd(container):
|
||
|
|
+ """
|
||
|
|
+ Test run isula with initrd:
|
||
|
|
+
|
||
|
|
+ 1) run isula with initrd
|
||
|
|
+ 2) execute shell command in isula
|
||
|
|
+ """
|
||
|
|
+ LOG.info("----------test_standvm_isula_initrd----------")
|
||
|
|
+ kata_container = container
|
||
|
|
+ container_id = None
|
||
|
|
+ try:
|
||
|
|
+ kata_container.replace_configuration(cig_name='configuration-initrd-stand.toml')
|
||
|
|
+ container_id = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="initrd1-hydropper-stand")
|
||
|
|
+ LOG.info("initrd-stand container id:%s", container_id)
|
||
|
|
+
|
||
|
|
+ session = kata_container.create_isula_shellsession("initrd1-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ls", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+
|
||
|
|
+ session.close()
|
||
|
|
+ kata_container.stop_isula("initrd1-hydropper-stand")
|
||
|
|
+ finally:
|
||
|
|
+ kata_container.remove_isula_force("initrd1-hydropper-stand")
|
||
|
|
+
|
||
|
|
+def test_standvm_isula_rootfs(container):
|
||
|
|
+ """
|
||
|
|
+ Test run isula with rootfs:
|
||
|
|
+
|
||
|
|
+ 1) run isula with rootfs
|
||
|
|
+ 2) execute shell command in isula
|
||
|
|
+ """
|
||
|
|
+ LOG.info("----------test_standvm_isula_rootfs----------")
|
||
|
|
+ kata_container = container
|
||
|
|
+ container_id = None
|
||
|
|
+ try:
|
||
|
|
+ kata_container.replace_configuration(cig_name='configuration-rootfs-stand.toml')
|
||
|
|
+ container_id = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="rootfs1-hydropper-stand")
|
||
|
|
+ LOG.info("rootfs-stand container id:%s", container_id)
|
||
|
|
+
|
||
|
|
+ session = kata_container.create_isula_shellsession("rootfs1-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ls", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+
|
||
|
|
+ session.close()
|
||
|
|
+ kata_container.stop_isula("rootfs1-hydropper-stand")
|
||
|
|
+ finally:
|
||
|
|
+ kata_container.remove_isula_force("rootfs1-hydropper-stand")
|
||
|
|
+
|
||
|
|
+def test_standvm_isula_template(container):
|
||
|
|
+ """
|
||
|
|
+ Test run isula with template:
|
||
|
|
+
|
||
|
|
+ 1) run template isula and create a template auto matically
|
||
|
|
+ 2) assert template has been created.
|
||
|
|
+ 3) run a new isula container from template
|
||
|
|
+ """
|
||
|
|
+ LOG.info("----------test_standvm_isula_template----------")
|
||
|
|
+ kata_container = container
|
||
|
|
+ container_id1 = container_id2 = None
|
||
|
|
+ if os.path.exists("/run/vc/vm/template/"):
|
||
|
|
+ subprocess.run("rm -rf /run/vc/vm/template/", shell=True, check=True)
|
||
|
|
+ try:
|
||
|
|
+ kata_container.replace_configuration(cig_name='configuration-template-stand.toml')
|
||
|
|
+ container_id1 = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="template1-hydropper-stand")
|
||
|
|
+ LOG.info("template container id:%s", container_id1)
|
||
|
|
+ session = kata_container.create_isula_shellsession("template1-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ls", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+ session.close()
|
||
|
|
+
|
||
|
|
+ assert os.path.exists("/run/vc/vm/template/")
|
||
|
|
+
|
||
|
|
+ container_id2 = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="template2-hydropper-stand")
|
||
|
|
+ LOG.info("run container from template, id:%s", container_id2)
|
||
|
|
+ session = kata_container.create_isula_shellsession("template2-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ls", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+ session.close()
|
||
|
|
+
|
||
|
|
+ kata_container.stop_isula("template1-hydropper-stand")
|
||
|
|
+ kata_container.stop_isula("template2-hydropper-stand")
|
||
|
|
+ finally:
|
||
|
|
+ kata_container.remove_isula_force("template1-hydropper-stand")
|
||
|
|
+ kata_container.remove_isula_force("template2-hydropper-stand")
|
||
|
|
+ if os.path.exists("/run/vc/vm/template/"):
|
||
|
|
+ subprocess.run("rm -rf /run/vc/vm/template/", shell=True, check=True)
|
||
|
|
+
|
||
|
|
+def test_standvm_isula_sandbox(container):
|
||
|
|
+ """
|
||
|
|
+ Test run isula with sandbox:
|
||
|
|
+
|
||
|
|
+ 1) run podsandbox container firstly.
|
||
|
|
+ 2) run a new container in podsanbox.
|
||
|
|
+ """
|
||
|
|
+ LOG.info("----------test_standvm_isula_sandbox----------")
|
||
|
|
+ kata_container = container
|
||
|
|
+ container_id = podsandbox_id = None
|
||
|
|
+ try:
|
||
|
|
+ kata_container.replace_configuration(cig_name='configuration-initrd-stand.toml')
|
||
|
|
+ podsandbox_id = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="sandbox1-hydropper-stand",
|
||
|
|
+ annotation="io.kubernetes.docker.type=podsandbox")
|
||
|
|
+ LOG.info("podsandbox container id:%s", podsandbox_id)
|
||
|
|
+
|
||
|
|
+ podsandbox_id = podsandbox_id.strip('\n')
|
||
|
|
+ container_id = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="sandbox2-hydropper-stand",
|
||
|
|
+ annotation=["io.kubernetes.docker.type=container",
|
||
|
|
+ ("io.kubernetes.sandbox.id=%s" % podsandbox_id)])
|
||
|
|
+ LOG.info("container id:%s", container_id)
|
||
|
|
+ session = kata_container.create_isula_shellsession("sandbox2-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ls", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+ session.close()
|
||
|
|
+
|
||
|
|
+ kata_container.stop_isula("sandbox2-hydropper-stand")
|
||
|
|
+ kata_container.stop_isula("sandbox1-hydropper-stand")
|
||
|
|
+ finally:
|
||
|
|
+ kata_container.remove_isula_force("sandbox2-hydropper-stand")
|
||
|
|
+ kata_container.remove_isula_force("sandbox1-hydropper-stand")
|
||
|
|
+
|
||
|
|
+@pytest.mark.skip
|
||
|
|
+@pytest.mark.parametrize("net_type, bdf, pf_name",
|
||
|
|
+ [('1822', '0000:03:00.0', 'enp3s0')])
|
||
|
|
+def test_standvm_isula_vfionet(container, net_type, bdf, pf_name):
|
||
|
|
+ """
|
||
|
|
+ Test run isula with vfio net device:
|
||
|
|
+ """
|
||
|
|
+ LOG.info("----------test_standvm_isula_vfionet----------")
|
||
|
|
+ kata_container = container
|
||
|
|
+ container_id = None
|
||
|
|
+ vf_bdf = bdf.split('.')[0] + '.1'
|
||
|
|
+ try:
|
||
|
|
+ kata_container.replace_configuration(cig_name='configuration-initrd-stand.toml')
|
||
|
|
+ utils.config_host_vfio(net_type=net_type, number='2', bdf=bdf)
|
||
|
|
+ utils.check_vf(pf_name=pf_name)
|
||
|
|
+ subprocess.run("modprobe vfio-pci", shell=True, check=True)
|
||
|
|
+ utils.rebind_vfio_pci(bdf=vf_bdf)
|
||
|
|
+ iommu_group = utils.get_iommu_group(vf_bdf)
|
||
|
|
+ container_id = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ device="/dev/vfio/%s" % iommu_group,
|
||
|
|
+ net="none",
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="vfionet1-hydropper-stand")
|
||
|
|
+ LOG.info("vfio net container id:%s", container_id)
|
||
|
|
+
|
||
|
|
+ session = kata_container.create_isula_shellsession("vfionet1-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ip a", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+
|
||
|
|
+ session.close()
|
||
|
|
+ kata_container.stop_isula("vfionet1-hydropper-stand")
|
||
|
|
+ finally:
|
||
|
|
+ utils.clean_vf(bdf=bdf)
|
||
|
|
+ kata_container.remove_isula_force("vfionet1-hydropper-stand")
|
||
|
|
+
|
||
|
|
+@pytest.mark.skip
|
||
|
|
+def test_standvm_isula_virtiofs(container):
|
||
|
|
+ """
|
||
|
|
+ Test run isula with virtio fs:
|
||
|
|
+ """
|
||
|
|
+ LOG.info("----------test_standvm_isula_virtiofs----------")
|
||
|
|
+ kata_container = container
|
||
|
|
+ container_id = None
|
||
|
|
+ test_dir = "/tmp/hydropper_virtio_fs"
|
||
|
|
+ if not os.path.exists(test_dir):
|
||
|
|
+ subprocess.run("mkdir %s" % test_dir, shell=True, check=True)
|
||
|
|
+ subprocess.run("touch %s/hydropper1.log" % test_dir, shell=True, check=True)
|
||
|
|
+ try:
|
||
|
|
+ kata_container.replace_configuration(cig_name='configuration-virtiofs-stand.toml')
|
||
|
|
+ container_id = kata_container.run_isula(options="-tid",
|
||
|
|
+ runtime="io.containerd.kata.v2",
|
||
|
|
+ net="none -v %s:/tmp/" % test_dir,
|
||
|
|
+ image="busybox:latest",
|
||
|
|
+ name="virtiofs1-hydropper-stand")
|
||
|
|
+ LOG.info("virtio fs container id:%s", container_id)
|
||
|
|
+
|
||
|
|
+ session = kata_container.create_isula_shellsession("virtiofs1-hydropper-stand")
|
||
|
|
+ status, _ = session.cmd_status_output("ls /tmp/hydropper1.log", timeout=SHELL_TIMEOUT)
|
||
|
|
+ assert status == 0
|
||
|
|
+
|
||
|
|
+ session.close()
|
||
|
|
+ kata_container.stop_isula("virtiofs1-hydropper-stand")
|
||
|
|
+ finally:
|
||
|
|
+ kata_container.remove_isula_force("virtiofs1-hydropper-stand")
|
||
|
|
+ subprocess.run("rm -rf /tmp/hydropper_virtio_fs", shell=True, check=True)
|
||
|
|
diff --git a/tests/hydropper/testcases/standvm/functional/test_standvm_vfio.py b/tests/hydropper/testcases/standvm/functional/test_standvm_vfio.py
|
||
|
|
index e6ca2b3..dc399a5 100644
|
||
|
|
--- a/tests/hydropper/testcases/standvm/functional/test_standvm_vfio.py
|
||
|
|
+++ b/tests/hydropper/testcases/standvm/functional/test_standvm_vfio.py
|
||
|
|
@@ -15,33 +15,13 @@ import logging
|
||
|
|
import pytest
|
||
|
|
import platform
|
||
|
|
from subprocess import run
|
||
|
|
-
|
||
|
|
+import utils.utils_common as utils
|
||
|
|
from utils.utils_logging import TestLog
|
||
|
|
|
||
|
|
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
|
||
|
|
logging.basicConfig(filename="/var/log/pytest.log", level=logging.DEBUG, format=LOG_FORMAT)
|
||
|
|
LOG = TestLog.get_global_log()
|
||
|
|
|
||
|
|
-def config_host_vfio(net_type, number, bdf):
|
||
|
|
- """configure vf in host"""
|
||
|
|
- ret = run("lspci -v | grep 'Eth' | grep %s" % net_type, shell=True, check=True).stdout
|
||
|
|
- LOG.debug(ret)
|
||
|
|
- ret = run("echo %s > /sys/bus/pci/devices/%s/sriov_numvfs" % (number, bdf), shell=True, check=True)
|
||
|
|
-
|
||
|
|
-def rebind_vfio_pci(bdf):
|
||
|
|
- """unbind old driver and bind a new one"""
|
||
|
|
- run("echo %s > /sys/bus/pci/devices/%s/driver/unbind" % (bdf, bdf), shell=True, check=True)
|
||
|
|
- run("echo `lspci -ns %s | awk -F':| ' '{print $5\" \"$6}'` > /sys/bus/pci/drivers/vfio-pci/new_id"\
|
||
|
|
- %bdf, shell=True, check=True)
|
||
|
|
-
|
||
|
|
-def check_vf(pf_name):
|
||
|
|
- """check whether vf is enabled"""
|
||
|
|
- run("ip link show %s | grep vf" % pf_name, shell=True, check=True)
|
||
|
|
-
|
||
|
|
-def clean_vf(bdf):
|
||
|
|
- """clean host vf"""
|
||
|
|
- ret = run("echo 0 > /sys/bus/pci/devices/%s/sriov_numvfs" % bdf, shell=True, check=True)
|
||
|
|
-
|
||
|
|
@pytest.mark.standvm_accept
|
||
|
|
@pytest.mark.parametrize("host_ip, net_type, bdf, pf_name",
|
||
|
|
[('9.13.7.139', '1822', '0000:03:00.0', 'enp3s0')])
|
||
|
|
@@ -57,11 +37,11 @@ def test_standvm_vfio_net(standvm, host_ip, net_type, bdf, pf_name):
|
||
|
|
flag = False
|
||
|
|
|
||
|
|
testvm = standvm
|
||
|
|
- config_host_vfio(net_type=net_type, number='2', bdf=bdf)
|
||
|
|
+ utils.config_host_vfio(net_type=net_type, number='2', bdf=bdf)
|
||
|
|
try:
|
||
|
|
- check_vf(pf_name=pf_name)
|
||
|
|
+ utils.check_vf(pf_name=pf_name)
|
||
|
|
run("modprobe vfio-pci", shell=True, check=True)
|
||
|
|
- rebind_vfio_pci(bdf=vf_bdf)
|
||
|
|
+ utils.rebind_vfio_pci(bdf=vf_bdf)
|
||
|
|
testvm.basic_config(vfio=True, bdf=vf_bdf)
|
||
|
|
testvm.launch()
|
||
|
|
_cmd = "ip a | awk '{ print $2 }' | cut -d ':' -f 1"
|
||
|
|
@@ -80,7 +60,7 @@ def test_standvm_vfio_net(standvm, host_ip, net_type, bdf, pf_name):
|
||
|
|
assert flag == True
|
||
|
|
finally:
|
||
|
|
testvm.shutdown()
|
||
|
|
- clean_vf(bdf=bdf)
|
||
|
|
+ utils.clean_vf(bdf=bdf)
|
||
|
|
|
||
|
|
@pytest.mark.standvm_accept
|
||
|
|
@pytest.mark.parametrize("bdf",[('0000:08:00.0')])
|
||
|
|
@@ -95,7 +75,7 @@ def test_standvm_vfio_ssd(standvm, bdf):
|
||
|
|
testvm = standvm
|
||
|
|
run("lspci | grep 'Non-Volatile memory'", shell=True, check=True)
|
||
|
|
run("modprobe vfio-pci", shell=True, check=True)
|
||
|
|
- rebind_vfio_pci(bdf=bdf)
|
||
|
|
+ utils.rebind_vfio_pci(bdf=bdf)
|
||
|
|
testvm.basic_config(vfio=True, bdf=bdf)
|
||
|
|
testvm.launch()
|
||
|
|
session = testvm.create_ssh_session()
|
||
|
|
@@ -111,4 +91,4 @@ def test_standvm_vfio_ssd(standvm, bdf):
|
||
|
|
assert ret == 0
|
||
|
|
|
||
|
|
session.close()
|
||
|
|
- testvm.shutdown()
|
||
|
|
\ No newline at end of file
|
||
|
|
+ testvm.shutdown()
|
||
|
|
diff --git a/tests/hydropper/utils/utils_common.py b/tests/hydropper/utils/utils_common.py
|
||
|
|
index 949cb5c..7713bef 100644
|
||
|
|
--- a/tests/hydropper/utils/utils_common.py
|
||
|
|
+++ b/tests/hydropper/utils/utils_common.py
|
||
|
|
@@ -14,6 +14,8 @@ import os
|
||
|
|
import errno
|
||
|
|
import ctypes
|
||
|
|
import shutil
|
||
|
|
+from subprocess import run
|
||
|
|
+from subprocess import PIPE
|
||
|
|
from utils.utils_logging import TestLog
|
||
|
|
|
||
|
|
LOG = TestLog.get_global_log()
|
||
|
|
@@ -57,4 +59,31 @@ def get_timestamp(timestamp):
|
||
|
|
minute = int(datetime.split(':')[1])
|
||
|
|
second = int(datetime.split(':')[2])
|
||
|
|
|
||
|
|
- return float(str(second + minute * 60 + hour * 60 * 24) + '.' + mill)
|
||
|
|
\ No newline at end of file
|
||
|
|
+ return float(str(second + minute * 60 + hour * 60 * 24) + '.' + mill)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def config_host_vfio(net_type, number, bdf):
|
||
|
|
+ """configure vf in host"""
|
||
|
|
+ ret = run("lspci -v | grep 'Eth' | grep %s" % net_type, shell=True, check=True).stdout
|
||
|
|
+ LOG.debug(ret)
|
||
|
|
+ ret = run("echo %s > /sys/bus/pci/devices/%s/sriov_numvfs" % (number, bdf), shell=True, check=True)
|
||
|
|
+
|
||
|
|
+def rebind_vfio_pci(bdf):
|
||
|
|
+ """unbind old driver and bind a new one"""
|
||
|
|
+ run("echo %s > /sys/bus/pci/devices/%s/driver/unbind" % (bdf, bdf), shell=True, check=True)
|
||
|
|
+ run("echo `lspci -ns %s | awk -F':| ' '{print $5\" \"$6}'` > /sys/bus/pci/drivers/vfio-pci/new_id"\
|
||
|
|
+ %bdf, shell=True, check=True)
|
||
|
|
+
|
||
|
|
+def check_vf(pf_name):
|
||
|
|
+ """check whether vf is enabled"""
|
||
|
|
+ run("ip link show %s | grep vf" % pf_name, shell=True, check=True)
|
||
|
|
+
|
||
|
|
+def clean_vf(bdf):
|
||
|
|
+ """clean host vf"""
|
||
|
|
+ ret = run("echo 0 > /sys/bus/pci/devices/%s/sriov_numvfs" % bdf, shell=True, check=True)
|
||
|
|
+
|
||
|
|
+def get_iommu_group(bdf):
|
||
|
|
+ """get iommu group id"""
|
||
|
|
+ read_cmd = "readlink /sys/bus/pci/devices/%s/iommu_group" % bdf
|
||
|
|
+ return run(read_cmd, shell=True, check=True, stdout=PIPE) \
|
||
|
|
+ .stdout.decode('utf-8').splitlines()[0].split('/')[-1]
|
||
|
|
--
|
||
|
|
2.25.1
|
||
|
|
|