Add get_volumesnapshots keywords

- Add get_volumesnapshots keywords
- Fix k8s_table_parser_base.py substring postfix issue
- Add unit test kubectl_get_volumesnapshots_table_parser_test.py

Change-Id: I60784f722da5d87cee7efc86be6b2939a24e0bc2
Signed-off-by: ppeng <peng.peng@windriver.com>
This commit is contained in:
ppeng
2025-10-09 16:07:05 -04:00
parent 89307ac3cf
commit 6e2e722491
6 changed files with 447 additions and 9 deletions

View File

@@ -1,3 +1,4 @@
import re
from typing import List from typing import List
from framework.exceptions.keyword_exception import KeywordException from framework.exceptions.keyword_exception import KeywordException
@@ -8,21 +9,25 @@ from keywords.k8s.k8s_table_parser_header import K8sTableParserHeader
class K8sTableParserBase: class K8sTableParserBase:
""" """
Base Class for parsing the output of Table-Like k8s commands. Base Class for parsing the output of Table-Like k8s commands.
This class shouldn't be used directly. Instead, it should be inherited by specific k8s table parser implementations. This class shouldn't be used directly. Instead, it should be inherited by specific k8s table parser implementations.
See KubectlGetPodsTableParser as an example. See KubectlGetPodsTableParser as an example.
""" """
def __init__(self, k8s_output): def __init__(self, k8s_output: str):
""" """
Constructor Constructor
Args: Args:
k8s_output: The raw String output of a kubernetes command that returns a table. k8s_output (str): The raw String output of a kubernetes command that returns a table.
""" """
self.k8s_output = k8s_output self.k8s_output = k8s_output
self.possible_headers = [] # This needs to be defined in child classes of K8sTableParser self.possible_headers = [] # This needs to be defined in child classes of K8sTableParser
def get_output_values_list(self): def get_output_values_list(self):
""" """
get_output_values_list
This function will take the raw String output of a kubernetes command that returns a table This function will take the raw String output of a kubernetes command that returns a table
and will parse it into a list of dictionaries. For example, if self.k8s_output is: and will parse it into a list of dictionaries. For example, if self.k8s_output is:
@@ -38,7 +43,6 @@ class K8sTableParserBase:
{'NAME': 'default', 'STATUS': 'Active', 'AGE': '18d'}] {'NAME': 'default', 'STATUS': 'Active', 'AGE': '18d'}]
""" """
if not self.possible_headers: if not self.possible_headers:
get_logger().log_error("There are no 'possible_headers' defined. Please use the specific child class of the k8s_table_parser that has the headers that you need.") get_logger().log_error("There are no 'possible_headers' defined. Please use the specific child class of the k8s_table_parser that has the headers that you need.")
raise KeywordException("Undefined 'possible_headers'.") raise KeywordException("Undefined 'possible_headers'.")
@@ -71,10 +75,12 @@ class K8sTableParserBase:
def get_headers(self, line: str) -> List[K8sTableParserHeader]: def get_headers(self, line: str) -> List[K8sTableParserHeader]:
""" """
This function will extract the headers from the header line passed in. This function will extract the headers from the header line passed in.
Args:
line: Line containing all the headers to be parsed.
Returns: List of K8sTableParserHeader that have been found, in order. Args:
line (str): Line containing all the headers to be parsed.
Returns:
List[K8sTableParserHeader]: List of K8sTableParserHeader that have been found, in order.
""" """
headers = [] headers = []
@@ -83,9 +89,10 @@ class K8sTableParserBase:
for header in self.possible_headers: for header in self.possible_headers:
if header in line: if header in line:
# Find the header followed by a space or end of line. # This is to avoid headers that are substrings of other headers, either substring as prefix or postfix.
# This is to avoid headers that are substrings of other headers. pattern = rf"\b{header}\b"
header_index = line.find(header + " ") match = re.search(pattern, line)
header_index = match.start()
header_index_last = line.find(header + "\n") header_index_last = line.find(header + "\n")
index = max(header_index, header_index_last) index = max(header_index, header_index_last)

View File

@@ -0,0 +1,70 @@
import time
from framework.ssh.ssh_connection import SSHConnection
from keywords.base_keyword import BaseKeyword
from keywords.k8s.k8s_command_wrapper import export_k8s_config
from keywords.k8s.volumesnapshots.object.kubectl_get_volumesnapshots_output import KubectlGetVolumesnapshotsOutput
class KubectlGetVolumesnapshotsKeywords(BaseKeyword):
"""
Class for 'kubectl get volumesnapshots.snapshot.storage.k8s.io' keywords
"""
def __init__(self, ssh_connection: SSHConnection):
"""
Initialize the KubectlGetVolumesnapshotsKeywords class.
Args:
ssh_connection (SSHConnection): An SSH connection object to the target system.
"""
self.ssh_connection = ssh_connection
def get_volumesnapshots(self, namespace: str = None, label: str = None) -> KubectlGetVolumesnapshotsOutput:
"""
Gets the k8s volumesnapshots that are available using '-o wide'.
Args:
namespace(str, optional): The namespace to search for volumesnapshots. If None, it will search in all namespaces.
label (str, optional): The label to search for volumesnapshots.
Returns:
KubectlGetVolumesnapshotsOutput: An object containing the parsed output of the command.
"""
arg_namespace = ""
if namespace:
arg_namespace = f"-n {namespace}"
kubectl_get_volumesnapshots_output = self.ssh_connection.send(export_k8s_config(f"kubectl {arg_namespace} -o wide get volumesnapshots.snapshot.storage.k8s.io"))
self.validate_success_return_code(self.ssh_connection)
volumesnapshots_list_output = KubectlGetVolumesnapshotsOutput(kubectl_get_volumesnapshots_output)
return volumesnapshots_list_output
def wait_for_volumesnapshot_status(self, volumesnapshot_name: str, expected_status: str, namespace: str = None, timeout: int = 600) -> bool:
"""
Waits timeout amount of time for the given volumesnapshot to be in the given status
Args:
volumesnapshot_name (str): the volumesnapshot name
expected_status (str): the expected status
namespace (str): the namespace
timeout (int): the timeout in secs
Returns:
bool: True if the volumesnapshot is in the expected status
"""
volumesnapshot_status_timeout = time.time() + timeout
while time.time() < volumesnapshot_status_timeout:
volumesnapshots_output = self.get_volumesnapshots()
if volumesnapshots_output:
volumesnapshot_status = self.get_volumesnapshots(namespace).get_volumesnapshot(volumesnapshot_name).get_ready_to_use()
if volumesnapshot_status == expected_status:
return True
time.sleep(5)
raise ValueError(f"volumesnapshot is not at expected status {expected_status}, after {timeout}s.")

View File

@@ -0,0 +1,84 @@
from keywords.k8s.volumesnapshots.object.kubectl_get_volumesnapshots_table_parser import KubectlGetVolumesnapshotsTableParser
from keywords.k8s.volumesnapshots.object.kubectl_volumesnapshot_object import KubectlVolumesnapshotObject
class KubectlGetVolumesnapshotsOutput:
"""
A class to interact with and retrieve information about Kubernetes volumesnapshots.snapshot.storage.k8s.io.
This class provides methods to filter and retrieve volumesnapshot information
using the `kubectl` command output.
"""
def __init__(self, kubectl_get_volumesnapshots_output: str):
"""
Constructor
Args:
kubectl_get_volumesnapshots_output (str): Raw string output from running a "kubectl get volumesnapshots" command.
"""
self.kubectl_volumesnapshot: [KubectlVolumesnapshotObject] = []
kubectl_get_volumesnapshots_table_parser = KubectlGetVolumesnapshotsTableParser(kubectl_get_volumesnapshots_output)
output_values_list = kubectl_get_volumesnapshots_table_parser.get_output_values_list()
for volumesnapshot_dict in output_values_list:
if "NAME" not in volumesnapshot_dict:
raise ValueError(f"There is no NAME associated with the volumesnapshot: {volumesnapshot_dict}")
volumesnapshot = KubectlVolumesnapshotObject(volumesnapshot_dict["NAME"])
if "READYTOUSE" in volumesnapshot_dict:
volumesnapshot.set_ready_to_use(volumesnapshot_dict["READYTOUSE"])
if "SOURCEPVC" in volumesnapshot_dict:
volumesnapshot.set_source_pvc(volumesnapshot_dict["SOURCEPVC"])
if "SOURCESNAPSHOTCONTENT" in volumesnapshot_dict:
volumesnapshot.set_source_snapshot_content(volumesnapshot_dict["SOURCESNAPSHOTCONTENT"])
if "RESTORESIZE" in volumesnapshot_dict:
volumesnapshot.set_restore_size(volumesnapshot_dict["RESTORESIZE"])
if "SNAPSHOTCLASS" in volumesnapshot_dict:
volumesnapshot.set_snapshot_class(volumesnapshot_dict["SNAPSHOTCLASS"])
if "SNAPSHOTCONTENT" in volumesnapshot_dict:
volumesnapshot.set_snapshot_content(volumesnapshot_dict["SNAPSHOTCONTENT"])
if "CREATIONTIME" in volumesnapshot_dict:
volumesnapshot.set_creation_time(volumesnapshot_dict["CREATIONTIME"])
if "AGE" in volumesnapshot_dict:
volumesnapshot.set_age(volumesnapshot_dict["AGE"])
self.kubectl_volumesnapshot.append(volumesnapshot)
def get_volumesnapshot(self, volumesnapshot_name: str) -> KubectlVolumesnapshotObject:
"""
This function will get the volumesnapshot with the name specified from this get_volumesnapshots_output.
Args:
volumesnapshot_name (str): The name of the volumesnapshot of interest.
Returns:
KubectlVolumesnapshotObject: The volumesnapshot object with the name specified.
Raises:
ValueError: If the volumesnapshot with the specified name does not exist in the output.
"""
for volumesnapshot in self.kubectl_volumesnapshot:
if volumesnapshot.get_name() == volumesnapshot_name:
return volumesnapshot
else:
raise ValueError(f"There is no volumesnapshot with the name {volumesnapshot_name}.")
def get_volumesnapshots(self) -> [KubectlVolumesnapshotObject]:
"""
Gets all volumesnapshots.
Returns:
[KubectlVolumesnapshotObject]: A list of all volumesnapshots.
"""
return self.kubectl_volumesnapshot

View File

@@ -0,0 +1,27 @@
from keywords.k8s.k8s_table_parser_base import K8sTableParserBase
class KubectlGetVolumesnapshotsTableParser(K8sTableParserBase):
"""
Class for parsing the output of "kubectl get volumesnapshots.snapshot.storage.k8s.io" commands.
"""
def __init__(self, k8s_output: str):
"""
Constructor
Args:
k8s_output (str): The raw String output of a kubernetes command that returns a table.
"""
super().__init__(k8s_output)
self.possible_headers = [
"NAME",
"READYTOUSE",
"SOURCEPVC",
"SOURCESNAPSHOTCONTENT",
"RESTORESIZE",
"SNAPSHOTCLASS",
"SNAPSHOTCONTENT",
"CREATIONTIME",
"AGE",
]

View File

@@ -0,0 +1,221 @@
class KubectlVolumesnapshotObject:
"""
Class to hold attributes of a 'kubectl get volumesnapshots.snapshot.storage.k8s.io' snapshot entry.
"""
def __init__(self, name: str):
"""
Constructor
Args:
name (str): Name of the snapshot.
"""
self.name = name
self.ready_to_use = None
self.source_pvc = None
self.source_snapshot_content = None
self.restore_size = None
self.snapshot_class = None
self.snapshot_content = None
self.creation_time = None
self.age = None
def get_name(self) -> str:
"""
Getter for NAME entry
Returns: The name of the snapshot.
"""
return self.name
def set_ready_to_use(self, ready_to_use: str) -> None:
"""
Setter for READYTOUSE
Args:
ready_to_use (str): 'true' or 'false'
Returns: None
"""
self.ready_to_use = ready_to_use
def get_ready_to_use(self) -> str:
"""
Getter for READYTOUSE entry
"""
return self.ready_to_use
def set_source_pvc(self, source_pvc: str) -> None:
"""
Setter for SOURCEPVC
Args:
source_pvc (str): source_pvc
Returns: None
"""
self.source_pvc = source_pvc
def get_source_pvc(self) -> str:
"""
Getter for SOURCEPVC entry
"""
return self.source_pvc
def set_source_snapshot_content(self, source_snapshot_content: str) -> None:
"""
Setter for SOURCESNAPSHOTCONTENT
Args:
source_snapshot_content (str): source_snapshot_content
Returns: None
"""
self.source_snapshot_content = source_snapshot_content
def get_source_snapshot_content(self) -> str:
"""
Getter for SOURCESNAPSHOTCONTENT entry
"""
return self.source_snapshot_content
def set_restore_size(self, restore_size: str) -> None:
"""
Setter for RESTORESIZE
Args:
restore_size (str): restore_size
Returns: None
"""
self.restore_size = restore_size
def get_restore_size(self) -> str:
"""
Getter for RESTORESIZE entry
"""
return self.restore_size
def set_snapshot_class(self, snapshot_class: str) -> None:
"""
Setter for SNAPSHOTCLASS
Args:
snapshot_class (str): snapshot_class
Returns: None
"""
self.snapshot_class = snapshot_class
def get_snapshot_class(self) -> str:
"""
Getter for SNAPSHOTCLASS entry
"""
return self.snapshot_class
def set_snapshot_content(self, snapshot_content: str) -> None:
"""
Setter for SNAPSHOTCONTENT
Args:
snapshot_content (str): snapshot_content
Returns: None
"""
self.snapshot_content = snapshot_content
def get_snapshot_content(self) -> str:
"""
Getter for SNAPSHOTCONTENT entry
"""
return self.snapshot_content
def set_creation_time(self, creation_time: str) -> None:
"""
Setter for CREATIONTIME
Args:
creation_time (str): creation time
Returns: None
"""
self.creation_time = creation_time
def get_creation_time(self) -> str:
"""
Getter for CREATIONTIME entry
"""
return self.creation_time
def get_creation_time_in_minutes(self) -> int:
"""
Converts the creation_time of the snapshot into minutes.
Returns:
int: The creation_time of the snapshot in minutes.
"""
snapshot_creation_time = self.get_creation_time()
total_minutes = 0
if "m" in snapshot_creation_time:
minutes = int(snapshot_creation_time.split("m")[0])
total_minutes += minutes
if "h" in snapshot_creation_time:
hours = int(snapshot_creation_time.split("h")[0])
total_minutes += hours * 60
if "d" in snapshot_creation_time:
days = int(snapshot_creation_time.split("d")[0])
total_minutes += days * 1440
if "s" in snapshot_creation_time:
pass
return total_minutes
def set_age(self, age: str) -> None:
"""
Setter for AGE
Args:
age (str): ago
Returns: None
"""
self.age = age
def get_age(self) -> str:
"""
Getter for AGE entry
"""
return self.age
def get_age_in_minutes(self) -> int:
"""
Converts the age of the snapshot into minutes.
Returns:
int: The age of the snapshot in minutes.
"""
snapshot_age = self.get_age()
total_minutes = 0
if "m" in snapshot_age:
minutes = int(snapshot_age.split("m")[0])
total_minutes += minutes
if "h" in snapshot_age:
hours = int(snapshot_age.split("h")[0])
total_minutes += hours * 60
if "d" in snapshot_age:
days = int(snapshot_age.split("d")[0])
total_minutes += days * 1440
if "s" in snapshot_age:
pass
return total_minutes

View File

@@ -0,0 +1,29 @@
from keywords.k8s.volumesnapshots.object.kubectl_get_volumesnapshots_table_parser import KubectlGetVolumesnapshotsTableParser
def test_get_volumesnapshots_table_parser():
"""
Tests the k8s_get_volumesnapshots table parser
Parser k8s_get_volumesnapshots table
"""
get_volumesnapshots_output = (
"NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE\n",
"mcsi-powerstore-pvc-snapshot true pvol0 8Gi csi-powerstore-snapshot snapcontent-02da0df5-3e9e-4981-a693-f7ae7b03db4c 19m 19m\n",
)
table_parser = KubectlGetVolumesnapshotsTableParser(get_volumesnapshots_output)
output_values = table_parser.get_output_values_list()
assert len(output_values) == 1, "There are two entries in this get volumesnapshots table."
first_line = output_values[0]
assert first_line["NAME"] == "mcsi-powerstore-pvc-snapshot"
assert first_line["READYTOUSE"] == "true"
assert first_line["SOURCEPVC"] == "pvol0"
assert first_line["SOURCESNAPSHOTCONTENT"] == ""
assert first_line["RESTORESIZE"] == "8Gi"
assert first_line["SNAPSHOTCLASS"] == "csi-powerstore-snapshot"
assert first_line["SNAPSHOTCONTENT"] == "snapcontent-02da0df5-3e9e-4981-a693-f7ae7b03db4c"
assert first_line["CREATIONTIME"] == "19m"
assert first_line["AGE"] == "19m"