Skip to content

Commit dcda07a

Browse files
committed
Pull request #22: Release v2.7.0
Merge in SIE-BB/netapp-dataops-toolkit from release-v2.7.0 to release-v3.0.0 * commit '1f16f19afe627e9ddffe77b1efa341bf5a4758db': Remove note re: keyring env var from README Updates: - fixing path for mcp server - renaming gcnv tests readme file Replaced default values with None for some parameters in create_volume function using keyring_service_name from constants.py constants module as a source of truth replacing .env approach by module-level constants keyring module in setup.cfg remove .env file adding default service name while creating config default service_name for keyring, and user instructions replacing print statements with logger adding keyring to dependencies modifying base name .env for single source of truth remove decoding password using keyring to set and get ontap credentials
2 parents 7746bea + 1f16f19 commit dcda07a

7 files changed

Lines changed: 54 additions & 22 deletions

File tree

netapp_dataops_traditional/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ netapp_dataops_cli.py config
4242

4343
Note: The toolkit requires an ONTAP account with API access. The toolkit will use this account to access the ONTAP API. NetApp recommends using an SVM-level account. Usage of a cluster admin account should be avoided for security reasons.
4444

45+
> **Note: Keyring Service Configuration**
46+
>
47+
> The toolkit stores ONTAP credentials securely using your system's keyring service. By default, it uses the service name `netapp:dataops:ontap` to store and retrieve credentials.
48+
4549
#### Example Usage
4650

4751
```sh
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Constants used across the NetApp DataOps Toolkit."""
2+
3+
# Keyring configuration constants
4+
KEYRING_SERVICE_NAME = "netapp:dataops:ontap"

netapp_dataops_traditional/netapp_dataops/netapp_dataops_cli.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
sys.path.insert(0, "/root/netapp-dataops-toolkit/netapp_dataops_traditional/netapp_dataops")
1111

12+
import keyring
1213
from netapp_dataops import traditional
1314
from netapp_dataops.traditional import (
1415
clone_volume,
@@ -44,10 +45,10 @@
4445
)
4546

4647
from netapp_dataops.logging_utils import setup_logger
48+
from netapp_dataops.constants import KEYRING_SERVICE_NAME
4749

4850
logger = setup_logger(__name__)
4951

50-
5152
## Define contents of help text
5253
helpTextStandard = '''
5354
The NetApp DataOps Toolkit is a Python library that makes it simple for data scientists and data engineers to perform various data management tasks, such as provisioning a new data volume, near-instantaneously cloning a data volume, and near-instantaneously snapshotting a data volume for traceability/baselining.
@@ -634,14 +635,15 @@ def createConfig(configDirPath: str = "~/.netapp_dataops", configFilename: str =
634635

635636
# Prompt user to enter additional config details
636637
config["defaultAggregate"] = input("Enter aggregate to use by default when creating new FlexVol volumes: ")
637-
config["username"] = input("Enter ONTAP API username (Recommendation: Use SVM account): ")
638+
username = input("Enter ONTAP API username (Recommendation: Use SVM account): ")
638639
passwordString = getpass("Enter ONTAP API password (Recommendation: Use SVM account): ")
639640

640-
# Convert password to base64 enconding
641-
passwordBytes = passwordString.encode("ascii")
642-
passwordBase64Bytes = base64.b64encode(passwordBytes)
643-
config["password"] = passwordBase64Bytes.decode("ascii")
644-
641+
# Store the password securely using keyring
642+
if username is not None:
643+
keyring.set_password(KEYRING_SERVICE_NAME, "username", username)
644+
if passwordString is not None:
645+
keyring.set_password(KEYRING_SERVICE_NAME, "password", passwordString)
646+
645647
# Prompt user to enter value denoting whether or not to verify SSL cert when calling ONTAP API
646648
# Verify value entered; prompt user to re-enter if invalid
647649
while True:

netapp_dataops_traditional/netapp_dataops/traditional/__init__.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from concurrent.futures import ThreadPoolExecutor
1818
import boto3
1919
from botocore.client import Config as BotoConfig
20+
import keyring
2021
from netapp_ontap import config as netappConfig
2122
from netapp_ontap.error import NetAppRestError
2223
from netapp_ontap.host_connection import HostConnection as NetAppHostConnection
@@ -32,6 +33,7 @@
3233
import requests
3334
from tabulate import tabulate
3435
import yaml
36+
from netapp_dataops.constants import KEYRING_SERVICE_NAME
3537

3638
from netapp_dataops.logging_utils import setup_logger
3739

@@ -201,17 +203,17 @@ def _instantiate_connection(config: dict, connectionType: str = "ONTAP", print_o
201203
try:
202204
ontapClusterMgmtHostname = config["hostname"]
203205
ontapClusterAdminUsername = config["username"]
204-
ontapClusterAdminPasswordBase64 = config["password"]
206+
ontapClusterAdminPassword = config["password"]
205207
verifySSLCert = config["verifySSLCert"]
206208
except:
207209
if print_output:
208210
_print_invalid_config_error()
209211
raise InvalidConfigError()
210212

211213
# Decode base64-encoded password
212-
ontapClusterAdminPasswordBase64Bytes = ontapClusterAdminPasswordBase64.encode("ascii")
213-
ontapClusterAdminPasswordBytes = base64.b64decode(ontapClusterAdminPasswordBase64Bytes)
214-
ontapClusterAdminPassword = ontapClusterAdminPasswordBytes.decode("ascii")
214+
# ontapClusterAdminPasswordBase64Bytes = ontapClusterAdminPasswordBase64.encode("ascii")
215+
# ontapClusterAdminPasswordBytes = base64.b64decode(ontapClusterAdminPasswordBase64Bytes)
216+
# ontapClusterAdminPassword = ontapClusterAdminPasswordBytes.decode("ascii")
215217

216218
# Instantiate connection to ONTAP cluster
217219
netappConfig.CONNECTION = NetAppHostConnection(
@@ -253,6 +255,25 @@ def _retrieve_config(configDirPath: str = "~/.netapp_dataops", configFilename: s
253255
with open(configFilePath, 'r') as configFile:
254256
# Read connection details from config file; read into dict
255257
config = json.load(configFile)
258+
259+
# Retrieve username and password from os-default credential manager
260+
username = keyring.get_password(KEYRING_SERVICE_NAME, "username")
261+
password = keyring.get_password(KEYRING_SERVICE_NAME, "password")
262+
263+
if username:
264+
config["username"] = username
265+
else:
266+
if print_output:
267+
logger.error("Error: Missing username in credential manager.")
268+
raise InvalidConfigError()
269+
270+
if password:
271+
config["password"] = password
272+
else:
273+
if print_output:
274+
logger.error("Error: Missing password in credential manager.")
275+
raise InvalidConfigError()
276+
256277
except:
257278
if print_output:
258279
_print_invalid_config_error()
@@ -820,8 +841,8 @@ def create_snapshot(volume_name: str, cluster_name: str = None, svm_name: str =
820841

821842

822843
def create_volume(volume_name: str, volume_size: str, guarantee_space: bool = False, cluster_name: str = None, svm_name: str = None,
823-
volume_type: str = "flexvol", unix_permissions: str = "0777",
824-
unix_uid: str = "0", unix_gid: str = "0", export_policy: str = "default", snaplock_type: str = None,
844+
volume_type: str = None, unix_permissions: str = None,
845+
unix_uid: str = None, unix_gid: str = None, export_policy: str = None, snaplock_type: str = None,
825846
snapshot_policy: str = None, aggregate: str = None, mountpoint: str = None, junction: str = None, readonly: bool = False,
826847
print_output: bool = False, tiering_policy: str = None, vol_dp: bool = False):
827848
# Retrieve config details from config file

netapp_dataops_traditional/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ install_requires =
4242
boto3
4343
pyyaml
4444
fastmcp
45+
keyring
4546
python_requires = >=3.8,<3.14
4647

4748
[options.extras_require]
File renamed without changes.

netapp_dataops_traditional/tests/mcp/test_gcnv_mcp.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,24 @@
5757

5858
def test_mcp_server_instance_exists():
5959
"""Test that the MCP server instance is created correctly"""
60-
from netapp_dataops.netapp_dataops_gcnv_mcp import mcp
60+
from netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp import mcp
6161
assert mcp is not None
6262
assert hasattr(mcp, 'name')
6363
assert mcp.name == "NetApp DataOps Traditional GCNV Toolkit MCP"
6464

6565

6666
def test_main_function():
6767
"""Test the main entry point function"""
68-
with patch('netapp_dataops.netapp_dataops_gcnv_mcp.asyncio.run') as mock_run:
69-
with patch('netapp_dataops.netapp_dataops_gcnv_mcp.mcp.run') as mock_mcp_run:
70-
from netapp_dataops.netapp_dataops_gcnv_mcp import main
68+
with patch('netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp.asyncio.run') as mock_run:
69+
with patch('netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp.mcp.run') as mock_mcp_run:
70+
from netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp import main
7171
main()
7272
mock_run.assert_called_once_with(mock_mcp_run.return_value)
7373

7474

7575
def test_imports_from_traditional_gcnv():
7676
"""Test that the MCP server correctly imports from traditional GCNV module"""
77-
import netapp_dataops.netapp_dataops_gcnv_mcp as mcp_module
77+
import netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp as mcp_module
7878

7979
# Check that the required functions are imported
8080
assert hasattr(mcp_module, 'create_volume')
@@ -397,7 +397,7 @@ def test_error_message_patterns():
397397

398398
def test_mcp_module_structure():
399399
"""Test that the MCP module has expected structure"""
400-
import netapp_dataops.netapp_dataops_gcnv_mcp as mcp_module
400+
import netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp as mcp_module
401401

402402
# Check that key components exist
403403
assert hasattr(mcp_module, 'mcp')
@@ -417,7 +417,7 @@ def test_mcp_module_structure():
417417
def test_mcp_module_can_be_imported():
418418
"""Test that the MCP module can be imported without errors"""
419419
try:
420-
import netapp_dataops.netapp_dataops_gcnv_mcp
420+
import netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp
421421
# If we get here, import was successful
422422
assert True
423423
except ImportError as e:
@@ -426,7 +426,7 @@ def test_mcp_module_can_be_imported():
426426

427427
def test_logging_configuration():
428428
"""Test that logging is properly configured"""
429-
import netapp_dataops.netapp_dataops_gcnv_mcp as mcp_module
429+
import netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp as mcp_module
430430

431431
assert hasattr(mcp_module, 'logger')
432432
logger = mcp_module.logger
@@ -435,4 +435,4 @@ def test_logging_configuration():
435435
assert hasattr(logger, 'error')
436436
assert hasattr(logger, 'info')
437437
assert hasattr(logger, 'warning')
438-
assert logger.name == 'netapp_dataops.netapp_dataops_gcnv_mcp'
438+
assert logger.name == 'netapp_dataops.mcp_server.netapp_dataops_gcnv_mcp'

0 commit comments

Comments
 (0)