Skip to content

Commit 590bd9b

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

File tree

9 files changed

+235
-0
lines changed

9 files changed

+235
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to the Zowe Client Python SDK will be documented in this fil
88

99
- Turning off logger at the class-constructor level [#316](https://github.com/zowe/zowe-client-python-sdk/issues/316)
1010
- Added support for commonly used environmental variables, like `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE`. [#346](https://github.com/zowe/zowe-client-python-sdk/issues/346)
11+
- Add method to issue SSH commands. [#253](https://github.com/zowe/zowe-client-python-sdk/issues/253)
1112

1213
### Bug Fixes
1314

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:

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
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/zos_uss/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
z/OS UNIX System Services (USS) Package
2+
=======================================
3+
4+
Provides APIs to interact with z/OS UNIX System Services (USS) over SSH (using z/OSMF or other SSH connections).
5+
6+
Examples
7+
--------
8+
9+
### Issue a command in the z/OS USS environment
10+
11+
```
12+
from zowe.core_for_zowe_sdk import ProfileManager
13+
from zowe.zos_uss_for_zowe_sdk import Uss
14+
15+
profile = ProfileManager().load(profile_name="zosmf")
16+
17+
with Uss(profile) as uss:
18+
print(uss.execute_command(command="ls -la", cwd="/u/home"))
19+
```

src/zos_uss/setup.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Zowe Client Python SDK.
2+
3+
This program and the accompanying materials are made available under the terms of the
4+
Eclipse Public License v2.0 which accompanies this distribution, and is available at
5+
6+
https://www.eclipse.org/legal/epl-v20.html
7+
8+
SPDX-License-Identifier: EPL-2.0
9+
10+
Copyright Contributors to the Zowe Project.
11+
"""
12+
13+
import sys
14+
15+
from setuptools import find_namespace_packages, setup
16+
17+
sys.path.insert(0, "..")
18+
from _version import __version__
19+
from setup import resolve_sdk_dep
20+
21+
setup(
22+
name="zowe_zos_uss_for_zowe_sdk",
23+
version=__version__,
24+
description="Zowe Python SDK - z/OS UNIX System Services (USS) package",
25+
long_description=open("README.md", "r").read(),
26+
long_description_content_type="text/markdown",
27+
url="https://github.com/zowe/zowe-client-python-sdk",
28+
author="Zowe",
29+
author_email="[email protected]",
30+
license="EPL-2.0",
31+
classifiers=[
32+
"Programming Language :: Python :: 3",
33+
"Programming Language :: Python :: 3.9",
34+
"License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)",
35+
],
36+
install_requires=[resolve_sdk_dep("core", "~=" + __version__)],
37+
packages=find_namespace_packages(include=["zowe.*"]),
38+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Zowe Client Python SDK.
2+
3+
This program and the accompanying materials are made available under the terms of the
4+
Eclipse Public License v2.0 which accompanies this distribution, and is available at
5+
6+
https://www.eclipse.org/legal/epl-v20.html
7+
8+
SPDX-License-Identifier: EPL-2.0
9+
10+
Copyright Contributors to the Zowe Project.
11+
"""
12+
13+
from .uss import Uss
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Zowe Client Python SDK.
2+
3+
This program and the accompanying materials are made available under the terms of the
4+
Eclipse Public License v2.0 which accompanies this distribution, and is available at
5+
6+
https://www.eclipse.org/legal/epl-v20.html
7+
8+
SPDX-License-Identifier: EPL-2.0
9+
10+
Copyright Contributors to the Zowe Project.
11+
"""
12+
13+
from typing import Optional
14+
import paramiko
15+
from zowe.core_for_zowe_sdk import SdkApi
16+
17+
18+
class Uss(SdkApi):
19+
"""
20+
Class to interact with Unix System Services (USS) on z/OS via SSH.
21+
22+
Parameters
23+
----------
24+
connection : dict
25+
A dictionary containing SSH connection details like hostname, username, password, and port.
26+
log : bool
27+
Flag to enable or disable logging.
28+
"""
29+
30+
def __init__(self, connection: dict, log: bool = True):
31+
super().__init__(connection, "/zosmf/restuss", logger_name=__name__, log=log)
32+
self.connection = connection
33+
self.ssh_client = None
34+
35+
def connect(self):
36+
"""
37+
Establish an SSH connection using the connection details.
38+
"""
39+
self.ssh_client = paramiko.SSHClient()
40+
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
41+
self.ssh_client.connect(
42+
hostname=self.connection["host"],
43+
username=self.connection["user"],
44+
password=self.connection.get("password"),
45+
port=self.connection.get("port", 22),
46+
)
47+
48+
def disconnect(self):
49+
"""
50+
Close the SSH connection.
51+
"""
52+
if self.ssh_client:
53+
self.ssh_client.close()
54+
55+
def execute_command(self, command: str, cwd: Optional[str] = None):
56+
"""
57+
Execute a Unix command over SSH.
58+
59+
Parameters
60+
----------
61+
command : str
62+
The command to execute.
63+
cwd : Optional[str]
64+
The working directory for the command.
65+
66+
Returns
67+
-------
68+
tuple
69+
A tuple of (stdout, stderr).
70+
"""
71+
if cwd:
72+
command = f"cd {cwd} && {command}"
73+
stdin, stdout, stderr = self.ssh_client.exec_command(command)
74+
return stdout.read().decode(), stderr.read().decode()

tests/integration/test_zos_uss.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Integration tests for the Zowe Python SDK z/OS UNIX System Services (USS) package."""
2+
3+
import unittest
4+
from unittest.mock import patch, MagicMock
5+
from zowe.zos_uss_for_zowe_sdk import Uss
6+
7+
class TestUss(unittest.TestCase):
8+
def setUp(self):
9+
self.profile = {
10+
"hostname": "example.com",
11+
"username": "user",
12+
"password": "pass",
13+
"port": 22,
14+
}
15+
self.uss = Uss(self.profile)
16+
17+
@patch("paramiko.SSHClient")
18+
def test_connect(self, mock_ssh_client):
19+
mock_client = MagicMock()
20+
mock_ssh_client.return_value = mock_client
21+
self.uss.connect()
22+
mock_client.connect.assert_called_with(
23+
hostname="example.com", username="user", password="pass", port=22
24+
)
25+
26+
@patch("paramiko.SSHClient")
27+
def test_execute_command(self, mock_ssh_client):
28+
mock_client = MagicMock()
29+
mock_ssh_client.return_value = mock_client
30+
mock_stdout = MagicMock()
31+
mock_stdout.read.return_value = b"command output"
32+
mock_stderr = MagicMock()
33+
mock_stderr.read.return_value = b""
34+
mock_client.exec_command.return_value = (None, mock_stdout, mock_stderr)
35+
36+
self.uss.connect()
37+
output, error = self.uss.execute_command("ls -l")
38+
self.assertEqual(output, "command output")
39+
self.assertEqual(error, "")

tests/unit/test_zos_uss.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Unit tests for the Zowe Python SDK z/OS UNIX System Services (USS) package."""
2+
3+
import unittest
4+
from unittest.mock import patch, MagicMock
5+
6+
from zowe.zos_uss_for_zowe_sdk import Uss
7+
8+
9+
class TestUss(unittest.TestCase):
10+
def setUp(self):
11+
self.profile = {
12+
"host": "mock-url.com",
13+
"user": "Username",
14+
"password": "Password",
15+
"port": 22,
16+
}
17+
self.uss = Uss(connection=self.profile)
18+
19+
@patch("paramiko.SSHClient")
20+
def test_connect(self, mock_ssh_client):
21+
self.uss.connect()
22+
mock_ssh_client.assert_called_once()
23+
self.assertIsNotNone(self.uss.ssh_client)
24+
25+
@patch("paramiko.SSHClient")
26+
def test_disconnect(self, mock_ssh_client):
27+
mock_ssh = mock_ssh_client.return_value
28+
self.uss.ssh_client = mock_ssh
29+
self.uss.disconnect()
30+
mock_ssh.close.assert_called_once()
31+
32+
@patch("paramiko.SSHClient")
33+
def test_execute_command(self, mock_ssh_client):
34+
mock_ssh = mock_ssh_client.return_value
35+
mock_stdout = MagicMock()
36+
mock_stderr = MagicMock()
37+
mock_stdout.read.return_value = b"Command executed successfully"
38+
mock_stderr.read.return_value = b""
39+
mock_ssh.exec_command.return_value = (None, mock_stdout, mock_stderr)
40+
41+
self.uss.ssh_client = mock_ssh
42+
stdout, stderr = self.uss.execute_command("ls -la")
43+
self.assertEqual(stdout, "Command executed successfully")
44+
self.assertEqual(stderr, "")
45+
46+
47+
if __name__ == "__main__":
48+
unittest.main()

0 commit comments

Comments
 (0)