Skip to content

Commit 17da71f

Browse files
add method to issue ssh commands
Signed-off-by: Olamide Ojo <[email protected]>
1 parent 91366a0 commit 17da71f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1602
-428
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to the Zowe Client Python SDK will be documented in this file.
44

5+
## Recent Changes
6+
7+
- Add method to issue SSH commands. [#253](https://github.com/zowe/zowe-client-python-sdk/issues/253)
8+
59
## `1.0.0-dev22`
610

711
### Enhancements

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jsonschema
5454
pyyaml
5555
requests>=2.22
5656
urllib3
57+
paramiko
5758
```
5859

5960
It also has an optional dependency on the Zowe Secrets SDK for storing client secrets which can be installed with the `secrets` extra:

docs/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ These steps should help you to build the documentation
2626
- `npm run doc:install`
2727
5. Build and open the documentation:
2828
- `npm run doc:dev`
29+
30+
## Best practices
31+
32+
When using paramiko to interact with z/OS UNIX System Services (USS), it's important to consider encoding and special character handling.
33+
Since z/OS uses EBCDIC-based encodings (e.g., IBM-1047, IBM-037, etc.), some commands may return unexpected results when processed in a UTF-8 environment. This is because by default, paramiko reads responses in UTF-8, but z/OS USS may return data in an EBCDIC codepage.
34+
Certain special characters, such as ööö, 👍, or 🔟, may not be correctly interpreted if the encoding is mismatched.
35+
If you experience unexpected characters in output, check the terminal's encoding settings (local command on Linux).
36+
Some commands may alter the terminal's codepage, affecting subsequent outputs.
37+
For example, switching between ASCII and EBCDIC on mainframes can impact character interpretation.
38+
If a command affects encoding, reset it after execution.

docs/source/_ext/zowe_autodoc.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,15 @@ def main():
5858
if len(class_names) == 1:
5959
rst_name = f"{py_name[:-3]}.rst"
6060
rst_contents = render_template(
61-
CLASS_TEMPLATE, {"fullname": f"{sdk_name}.{pkg_name}.{class_names[0]}", "header": class_names[0]}
61+
CLASS_TEMPLATE,
62+
{
63+
"fullname": f"{sdk_name}.{pkg_name}.{class_names[0]}",
64+
"header": class_names[0],
65+
},
6266
)
63-
with open(f"docs/source/classes/{sdk_name}/{rst_name}", "w", encoding="utf-8") as f:
67+
with open(
68+
f"docs/source/classes/{sdk_name}/{rst_name}", "w", encoding="utf-8"
69+
) as f:
6470
f.write(rst_contents)
6571
rst_names.append(rst_name)
6672
elif len(class_names) > 1:
@@ -71,9 +77,16 @@ def main():
7177
rst_name = f"{class_name.lower()}.rst"
7278
rst_contents = render_template(
7379
CLASS_TEMPLATE,
74-
{"fullname": f"{sdk_name}.{pkg_name}.{module_name}.{class_name}", "header": class_name},
80+
{
81+
"fullname": f"{sdk_name}.{pkg_name}.{module_name}.{class_name}",
82+
"header": class_name,
83+
},
7584
)
76-
with open(f"docs/source/classes/{sdk_name}/{module_name}/{rst_name}", "w", encoding="utf-8") as f:
85+
with open(
86+
f"docs/source/classes/{sdk_name}/{module_name}/{rst_name}",
87+
"w",
88+
encoding="utf-8",
89+
) as f:
7790
f.write(rst_contents)
7891
child_rst_names.append(rst_name)
7992
rst_name = f"{module_name}/index.rst"
@@ -85,26 +98,36 @@ def main():
8598
"maxdepth": 2,
8699
},
87100
)
88-
with open(f"docs/source/classes/{sdk_name}/{rst_name}", "w", encoding="utf-8") as f:
101+
with open(
102+
f"docs/source/classes/{sdk_name}/{rst_name}", "w", encoding="utf-8"
103+
) as f:
89104
f.write(rst_contents)
90105
parent_rst_names.append(rst_name)
91106

92107
rst_contents = render_template(
93108
INDEX_TEMPLATE,
94109
{
95-
"filelist": "\n ".join(name[:-4] for name in rst_names + parent_rst_names),
110+
"filelist": "\n ".join(
111+
name[:-4] for name in rst_names + parent_rst_names
112+
),
96113
"header": pkg_name,
97114
"maxdepth": 2,
98115
},
99116
)
100-
with open(f"docs/source/classes/{sdk_name}/index.rst", "w", encoding="utf-8") as f:
117+
with open(
118+
f"docs/source/classes/{sdk_name}/index.rst", "w", encoding="utf-8"
119+
) as f:
101120
f.write(rst_contents)
102121
sdk_names.append(sdk_name)
103122
print("done")
104123

105124
rst_contents = render_template(
106125
INDEX_TEMPLATE,
107-
{"filelist": "\n ".join(f"{name}/index" for name in sdk_names), "header": "Classes", "maxdepth": 3},
126+
{
127+
"filelist": "\n ".join(f"{name}/index" for name in sdk_names),
128+
"header": "Classes",
129+
"maxdepth": 3,
130+
},
108131
)
109132
with open(f"docs/source/classes/index.rst", "w", encoding="utf-8") as f:
110133
f.write(rst_contents)

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ deepmerge==1.1.0
33
jsonschema==4.17.3
44
PyYAML==6.0.1
55
requests==2.32.0
6+
paramiko==3.5.0
67

78
# Dev deps
89
black
@@ -24,3 +25,4 @@ wheel
2425
-e ./src/zos_jobs
2526
-e ./src/zos_tso
2627
-e ./src/zosmf
28+
-e ./src/zos_uss

src/core/zowe/core_for_zowe_sdk/config_file.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
Copyright Contributors to the Zowe Project.
1111
"""
12+
1213
import json
1314
import os.path
1415
import re
@@ -29,7 +30,9 @@
2930

3031
HOME = os.path.expanduser("~")
3132
GLOBAL_CONFIG_LOCATION = os.path.join(HOME, ".zowe")
32-
GLOBAL_CONFIG_PATH = os.path.join(GLOBAL_CONFIG_LOCATION, f"{GLOBAL_CONFIG_NAME}.config.json")
33+
GLOBAL_CONFIG_PATH = os.path.join(
34+
GLOBAL_CONFIG_LOCATION, f"{GLOBAL_CONFIG_NAME}.config.json"
35+
)
3336
CURRENT_DIR = os.getcwd()
3437

3538
# Profile datatype is used by ConfigFile to return Profile Data along with
@@ -193,7 +196,9 @@ def schema_list(self, cwd: str = None) -> list:
193196
profile_props: dict = {}
194197
schema_json = dict(schema_json)
195198

196-
for props in schema_json["properties"]["profiles"]["patternProperties"]["^\\S*$"]["allOf"]:
199+
for props in schema_json["properties"]["profiles"]["patternProperties"][
200+
"^\\S*$"
201+
]["allOf"]:
197202
props = props["then"]
198203

199204
while "properties" in props:
@@ -234,14 +239,18 @@ def get_profile(
234239
self.init_from_file(validate_schema)
235240

236241
if profile_name is None and profile_type is None:
237-
self.__logger.error(f"Failed to load profile: profile_name and profile_type were not provided.")
242+
self.__logger.error(
243+
f"Failed to load profile: profile_name and profile_type were not provided."
244+
)
238245
raise ProfileNotFound(
239246
profile_name=profile_name,
240247
error_msg="Could not find profile as both profile_name and profile_type is not set.",
241248
)
242249

243250
if profile_name is None:
244-
profile_name = self.get_profilename_from_profiletype(profile_type=profile_type)
251+
profile_name = self.get_profilename_from_profiletype(
252+
profile_type=profile_type
253+
)
245254
props: dict = self.load_profile_properties(profile_name=profile_name)
246255

247256
return Profile(props, profile_name, self._missing_secure_props)
@@ -296,7 +305,9 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str:
296305
try:
297306
profilename = self.defaults[profile_type]
298307
except KeyError:
299-
self.__logger.warn(f"Given profile type '{profile_type}' has no default profile name")
308+
self.__logger.warn(
309+
f"Given profile type '{profile_type}' has no default profile name"
310+
)
300311
warnings.warn(
301312
f"Given profile type '{profile_type}' has no default profile name",
302313
ProfileParsingWarning,
@@ -318,7 +329,9 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str:
318329
)
319330

320331
# if no profile with matching type found, we raise an exception
321-
self.__logger.error(f"No profile with matching profile_type '{profile_type}' found")
332+
self.__logger.error(
333+
f"No profile with matching profile_type '{profile_type}' found"
334+
)
322335
raise ProfileNotFound(
323336
profile_name=profile_type,
324337
error_msg=f"No profile with matching profile_type '{profile_type}' found",
@@ -378,7 +391,9 @@ def load_profile_properties(self, profile_name: str) -> dict:
378391
secure_fields.extend(profile.get("secure", []))
379392
else:
380393
self.__logger.warning(f"Profile {profile_name} not found")
381-
warnings.warn(f"Profile {profile_name} not found", ProfileNotFoundWarning)
394+
warnings.warn(
395+
f"Profile {profile_name} not found", ProfileNotFoundWarning
396+
)
382397
lst.pop()
383398

384399
return props
@@ -399,7 +414,9 @@ def __load_secure_properties(self):
399414
else:
400415
break
401416

402-
def __extract_secure_properties(self, profiles_obj: dict, json_path: Optional[str] = "profiles") -> dict:
417+
def __extract_secure_properties(
418+
self, profiles_obj: dict, json_path: Optional[str] = "profiles"
419+
) -> dict:
403420
"""
404421
Extract secure properties from the profiles object for storage in the vault.
405422
@@ -420,11 +437,15 @@ def __extract_secure_properties(self, profiles_obj: dict, json_path: Optional[st
420437
for key, value in profiles_obj.items():
421438
for property_name in value.get("secure", []):
422439
if property_name in value.get("properties", {}):
423-
secure_props[f"{json_path}.{key}.properties.{property_name}"] = value["properties"].pop(
424-
property_name
440+
secure_props[f"{json_path}.{key}.properties.{property_name}"] = (
441+
value["properties"].pop(property_name)
425442
)
426443
if value.get("profiles"):
427-
secure_props.update(self.__extract_secure_properties(value["profiles"], f"{json_path}.{key}.profiles"))
444+
secure_props.update(
445+
self.__extract_secure_properties(
446+
value["profiles"], f"{json_path}.{key}.profiles"
447+
)
448+
)
428449
return secure_props
429450

430451
def __set_or_create_nested_profile(self, profile_name: str, profile_data: dict):
@@ -466,7 +487,9 @@ def __is_secure(self, json_path: str, property_name: str) -> bool:
466487
return property_name in profile["secure"]
467488
return False
468489

469-
def set_property(self, json_path: str, value: str, secure: Optional[bool] = None) -> None:
490+
def set_property(
491+
self, json_path: str, value: str, secure: Optional[bool] = None
492+
) -> None:
470493
"""
471494
Set a property in the profile, storing it securely if necessary.
472495
@@ -521,7 +544,9 @@ def set_profile(self, profile_path: str, profile_data: dict) -> None:
521544
secure_fields = profile_data["secure"]
522545
current_profile = self.find_profile(profile_name, self.profiles) or {}
523546
existing_secure_fields = current_profile.get("secure", [])
524-
new_secure_fields = [field for field in secure_fields if field not in existing_secure_fields]
547+
new_secure_fields = [
548+
field for field in secure_fields if field not in existing_secure_fields
549+
]
525550

526551
# Updating the 'secure' field of the profile with the combined list of secure fields
527552
profile_data["secure"] = existing_secure_fields + new_secure_fields
@@ -569,7 +594,11 @@ def get_profile_name_from_path(self, path: str) -> str:
569594
Returns the profile name
570595
"""
571596
segments = path.split(".")
572-
profile_name = ".".join(segments[i] for i in range(1, len(segments), 2) if segments[i - 1] != "properties")
597+
profile_name = ".".join(
598+
segments[i]
599+
for i in range(1, len(segments), 2)
600+
if segments[i - 1] != "properties"
601+
)
573602
return profile_name
574603

575604
def get_profile_path_from_name(self, short_path: str) -> str:

src/core/zowe/core_for_zowe_sdk/connection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ class ApiConnection:
3535
Missing connection argument.
3636
"""
3737

38-
def __init__(self, host_url: str, user: str, password: str, ssl_verification: bool = True):
38+
def __init__(
39+
self, host_url: str, user: str, password: str, ssl_verification: bool = True
40+
):
3941
__logger = Log.register_logger(__name__)
4042
if not host_url or not user or not password:
4143
__logger.error("Missing connection argument")

src/core/zowe/core_for_zowe_sdk/credential_manager.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,20 @@ def load_secure_props() -> None:
5050
return
5151

5252
try:
53-
secret_value = CredentialManager._get_credential(constants["ZoweServiceName"], constants["ZoweAccountName"])
53+
secret_value = CredentialManager._get_credential(
54+
constants["ZoweServiceName"], constants["ZoweAccountName"]
55+
)
5456
# Handle the case when secret_value is None
5557
if secret_value is None:
5658
return
5759

5860
except Exception as exc:
59-
CredentialManager.__logger.error(f"Fail to load secure profile {constants['ZoweServiceName']}")
60-
raise SecureProfileLoadFailed(constants["ZoweServiceName"], error_msg=str(exc)) from exc
61+
CredentialManager.__logger.error(
62+
f"Fail to load secure profile {constants['ZoweServiceName']}"
63+
)
64+
raise SecureProfileLoadFailed(
65+
constants["ZoweServiceName"], error_msg=str(exc)
66+
) from exc
6167

6268
secure_config: str
6369
secure_config = secret_value.encode()
@@ -74,12 +80,18 @@ def save_secure_props() -> None:
7480
credential = CredentialManager.secure_props
7581
# Check if credential is a non-empty string
7682
if credential:
77-
encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode()
83+
encoded_credential = base64.b64encode(
84+
commentjson.dumps(credential).encode()
85+
).decode()
7886
if sys.platform == "win32":
7987
# Delete the existing credential
80-
CredentialManager._delete_credential(constants["ZoweServiceName"], constants["ZoweAccountName"])
88+
CredentialManager._delete_credential(
89+
constants["ZoweServiceName"], constants["ZoweAccountName"]
90+
)
8191
CredentialManager._set_credential(
82-
constants["ZoweServiceName"], constants["ZoweAccountName"], encoded_credential
92+
constants["ZoweServiceName"],
93+
constants["ZoweAccountName"],
94+
encoded_credential,
8395
)
8496

8597
@staticmethod
@@ -110,21 +122,31 @@ def _get_credential(service_name: str, account_name: str) -> Optional[str]:
110122
else:
111123
encoded_credential += temp_value
112124
index += 1
113-
temp_value = keyring.get_password(service_name, f"{account_name}-{index}")
125+
temp_value = keyring.get_password(
126+
service_name, f"{account_name}-{index}"
127+
)
114128

115129
if encoded_credential is not None and encoded_credential.endswith("\0"):
116130
encoded_credential = encoded_credential[:-1]
117131

118132
return encoded_credential
119133

120134
@staticmethod
121-
def _set_credential(service_name: str, account_name: str, encoded_credential: str) -> None:
135+
def _set_credential(
136+
service_name: str, account_name: str, encoded_credential: str
137+
) -> None:
122138
# Check if the encoded credential exceeds the maximum length for win32
123-
if sys.platform == "win32" and len(encoded_credential) > constants["WIN32_CRED_MAX_STRING_LENGTH"]:
139+
if (
140+
sys.platform == "win32"
141+
and len(encoded_credential) > constants["WIN32_CRED_MAX_STRING_LENGTH"]
142+
):
124143
# Split the encoded credential string into chunks of maximum length
125144
chunk_size = constants["WIN32_CRED_MAX_STRING_LENGTH"]
126145
encoded_credential += "\0"
127-
chunks = [encoded_credential[i : i + chunk_size] for i in range(0, len(encoded_credential), chunk_size)]
146+
chunks = [
147+
encoded_credential[i : i + chunk_size]
148+
for i in range(0, len(encoded_credential), chunk_size)
149+
]
128150
# Set the individual chunks as separate keyring entries
129151
for index, chunk in enumerate(chunks, start=1):
130152
field_name = f"{account_name}-{index}"

0 commit comments

Comments
 (0)