Skip to content

Commit da79289

Browse files
Karnik, AarkinKarnik, Aarkin
authored andcommitted
Merge branch 'release-v3.0.0' of https://bitbucket.ngage.netapp.com/scm/sie-bb/netapp-dataops-toolkit into bugfix/blackduck
2 parents 51b7820 + d9d9718 commit da79289

7 files changed

Lines changed: 142 additions & 66 deletions

File tree

netapp_dataops_traditional/docs/ontap_readme.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,8 @@ team1_ws1 300.0GB 5% 285.0GB 15.04GB 2.87GB flex
367367

368368
The NetApp DataOps Toolkit can be used to mount an existing data volume on your local host. The command for mounting an existing volume locally is `netapp_dataops_cli.py mount volume`. If executed on a Linux host, this command must be run as root. It is usually not necessary to run this command as root on macOS hosts.
369369

370+
**Note:** When mounting volumes, the toolkit must be configured for the user executing the mount command. If running as root (using `sudo`), run `sudo netapp_dataops_cli.py config` to create a configuration file for the root user before attempting to mount volumes.
371+
370372
The following options/arguments are required:
371373

372374
```
@@ -401,6 +403,8 @@ Volume mounted successfully.
401403

402404
The NetApp DataOps Toolkit can be used to unmount an existing data volume that is currently on your local host. The command for unmounting an existing volume is `netapp_dataops_cli.py unmount volume`. If executed on a Linux host, this command must be run as root. It is usually not necessary to run this command as root on macOS hosts.
403405

406+
**Note:** When unmounting volumes, the toolkit must be configured for the user executing the unmount command. If running as root (using `sudo`), run `sudo netapp_dataops_cli.py config` to create a configuration file for the root user before attempting to unmount volumes.
407+
404408
The following options/arguments are required:
405409

406410
```
@@ -1663,6 +1667,9 @@ APIConnectionError # The storage system/service API returned an err
16631667
#### Mount an Existing Data Volume Locally
16641668

16651669
The NetApp DataOps Toolkit can be used to mount an existing data volume as "read-only" or "read-write" on your local host as part of any Python program or workflow. On Linux hosts, mounting requires root privileges, so any Python program that invokes this function must be run as root. It is usually not necessary to invoke this function as root on macOS hosts.
1670+
1671+
**Note:** When mounting volumes programmatically, the toolkit must be configured for the user executing the program. If the program runs as root (using `sudo`), run `sudo netapp_dataops_cli.py config` to create a configuration file for the root user before attempting to mount volumes.
1672+
16661673
##### Function Definition
16671674

16681675
```py
@@ -1698,6 +1705,8 @@ MountOperationError # The volume was not succesfully mounted locally
16981705

16991706
The NetApp DataOps Toolkit can be used to near-instantaneously unmount an existing data volume (that is currently mounted on your local host) as part of any Python program or workflow.
17001707

1708+
**Note:** When unmounting volumes programmatically, the toolkit must be configured for the user executing the program. If the program runs as root (using `sudo`), run `sudo netapp_dataops_cli.py config` to create a configuration file for the root user before attempting to unmount volumes.
1709+
17011710
##### Function Definition
17021711

17031712
```py

netapp_dataops_traditional/netapp_dataops/commands/create_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def _create_cifs_share(self) -> None:
373373
try:
374374
opts, _ = getopt.getopt(
375375
self.args[3:],
376-
"u:h:s:n:l:a:v:c:",
376+
"u:hs:n:l:a:v:c:",
377377
["cluster-name=", "help", "svm=", "name=", "properties=", "acls=", "volume=", "comment="]
378378
)
379379
except Exception as err:

netapp_dataops_traditional/netapp_dataops/commands/get_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _get_cifs_share(self) -> None:
5555
try:
5656
opts, args = getopt.getopt(
5757
self.args[3:],
58-
"u:h:s:n:",
58+
"u:hs:n:",
5959
["cluster-name=", "help", "svm=", "name="]
6060
)
6161
except Exception as err:

netapp_dataops_traditional/netapp_dataops/config/dataset_manager.py

Lines changed: 94 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,9 @@ def _setup_existing_root_volume(self, config: DatasetManagerConfig) -> None:
167167
volume_info = self._get_volume_info(root_volume_name)
168168

169169
if volume_info is None:
170-
logger.info(f" Warning: Volume '{root_volume_name}' not found on ONTAP.")
171-
logger.info(" Configuration has been saved. Please verify the volume name and try setup again.")
170+
logger.info(f" Note: Cannot validate volume '{root_volume_name}' on ONTAP at this time.")
171+
logger.info(" Configuration has been saved. Volume validation will occur when you use Dataset Manager.")
172+
# Skip mounting setup since we can't validate the volume
172173
return
173174

174175
# Check junction path
@@ -197,12 +198,39 @@ def _setup_existing_root_volume(self, config: DatasetManagerConfig) -> None:
197198
self._handle_root_volume_mounting(root_volume_name, root_mountpoint)
198199

199200
def _setup_new_root_volume(self, config: DatasetManagerConfig) -> None:
200-
"""Perform operations for new root volume after config is saved."""
201+
"""Perform operations for new root volume after config is saved.
202+
203+
Note: This method assumes the base ONTAP configuration has already been
204+
saved to disk before being called, as it needs to access ONTAP APIs.
205+
"""
201206
root_volume_name = config.root_volume_name
202207
root_mountpoint = config.root_mountpoint
203208

204209
logger.info(f"\n Setting up new Dataset Manager root volume '{root_volume_name}'...")
205210

211+
# Verify we can connect to ONTAP before proceeding
212+
try:
213+
from ..traditional.core import _retrieve_config
214+
from ..traditional.exceptions import InvalidConfigError
215+
216+
# Try to retrieve config to verify it exists and is valid
217+
try:
218+
ontap_config = _retrieve_config(print_output=False)
219+
# Verify we have the minimum required fields
220+
if not all(k in ontap_config for k in ['hostname', 'svm', 'dataLif']):
221+
raise InvalidConfigError("Missing required ONTAP configuration fields")
222+
except (InvalidConfigError, FileNotFoundError, KeyError) as e:
223+
logger.info(f" Note: Cannot create root volume - ONTAP configuration not available yet.")
224+
logger.info(f" Dataset Manager configuration has been saved.")
225+
logger.info(f" The root volume will be created when you first use Dataset Manager,")
226+
logger.info(f" or you can create it manually: netapp_dataops_cli.py create volume -n {root_volume_name} -s 1GB")
227+
return
228+
except Exception as e:
229+
logger.info(f" Note: Cannot validate ONTAP connection: {e}")
230+
logger.info(f" Dataset Manager configuration has been saved.")
231+
logger.info(f" Please create the root volume manually or reconfigure later.")
232+
return
233+
206234
while True: # Loop for retrying with different names if needed
207235
try:
208236
# Check if volume already exists
@@ -250,7 +278,10 @@ def _setup_new_root_volume(self, config: DatasetManagerConfig) -> None:
250278
self._handle_root_volume_mounting(root_volume_name, root_mountpoint)
251279

252280
def _get_volume_info(self, volume_name: str) -> Optional[Dict[str, Any]]:
253-
"""Get volume information from ONTAP."""
281+
"""Get volume information from ONTAP.
282+
283+
Returns None if ONTAP connection cannot be established or volume not found.
284+
"""
254285
try:
255286
# Use list_volumes to find the specific volume
256287
# Always suppress output to avoid displaying volume lists during user input
@@ -260,12 +291,17 @@ def _get_volume_info(self, volume_name: str) -> Optional[Dict[str, Any]]:
260291
return volume
261292
return None
262293
except Exception as e:
294+
# Could be a connection error or other issue
295+
# Return None to allow graceful handling by caller
263296
if self.print_output:
264-
logger.info(f"Error retrieving volume information: {e}")
297+
logger.info(f" Note: Could not retrieve volume information: {e}")
265298
return None
266299

267300
def _junction_path_exists(self, junction_path: str, exclude_volume: str = None) -> bool:
268-
"""Check if a junction path is already in use by any volume."""
301+
"""Check if a junction path is already in use by any volume.
302+
303+
Returns False if ONTAP connection cannot be established or on error.
304+
"""
269305
try:
270306
# Always suppress output to avoid displaying volume lists during validation
271307
volumes = volume_operations.list_volumes(print_output=False)
@@ -308,36 +344,31 @@ def _create_root_volume_on_ontap(self, volume_name: str) -> bool:
308344
return False
309345

310346
def _is_mounted(self, mountpoint: str) -> bool:
311-
"""Check if a mountpoint is currently in use."""
347+
"""Check if a mountpoint is currently in use (cross-platform)."""
312348
try:
313-
# Check if mountpoint utility is available
314-
self._check_required_utilities('mountpoint')
315-
316-
result = subprocess.run(['mountpoint', '-q', mountpoint],
317-
capture_output=True, text=True)
318-
return result.returncode == 0
319-
except RuntimeError as e:
320-
# Re-raise utility check errors
321-
raise
349+
# Use mount command which is available on both Linux and macOS
350+
result = subprocess.run(['mount'], capture_output=True, text=True)
351+
if result.returncode == 0:
352+
# Check if mountpoint appears in mount output
353+
for line in result.stdout.split('\n'):
354+
# Match " on <mountpoint> " pattern to avoid false positives
355+
if f" on {mountpoint} " in line or line.endswith(f" on {mountpoint}"):
356+
return True
357+
return False
322358
except Exception:
323359
return False
324360

325361
def _get_mount_target(self, mountpoint: str) -> str:
326-
"""Get what is currently mounted at the given mountpoint."""
362+
"""Get what is currently mounted at the given mountpoint (cross-platform)."""
327363
try:
328-
# Check if mount utility is available
329-
self._check_required_utilities('mount')
330-
331364
result = subprocess.run(['mount'], capture_output=True, text=True)
332365
if result.returncode == 0:
333366
for line in result.stdout.split('\n'):
334-
if f" {mountpoint} " in line:
367+
# Match " on <mountpoint> " pattern
368+
if f" on {mountpoint} " in line or line.endswith(f" on {mountpoint}"):
335369
# Extract the source (first part before " on ")
336370
return line.split(' on ')[0]
337371
return "unknown"
338-
except RuntimeError as e:
339-
# Re-raise utility check errors
340-
raise
341372
except Exception:
342373
return "unknown"
343374

@@ -365,6 +396,8 @@ def _handle_root_volume_mounting(self, volume_name: str, mountpoint: str) -> Non
365396
# Add to fstab FIRST, then mount using fstab entry
366397
if self._add_to_fstab(volume_name, mountpoint, expected_nfs_target):
367398
logger.info(f" Volume '{volume_name}' added to fstab")
399+
# Now mount the volume using the fstab entry
400+
self._mount_from_fstab(mountpoint)
368401

369402
def _add_to_fstab(self, volume_name: str, mountpoint: str, nfs_target: str) -> bool:
370403
"""Add volume to fstab first, then mount using fstab entry (following requirements)."""
@@ -392,6 +425,43 @@ def _add_to_fstab(self, volume_name: str, mountpoint: str, nfs_target: str) -> b
392425
logger.info(f" Error in fstab setup and mount: {e}")
393426
return False
394427

428+
def _mount_from_fstab(self, mountpoint: str) -> bool:
429+
"""
430+
Mount a volume using its fstab entry.
431+
432+
Args:
433+
mountpoint: The mountpoint path to mount
434+
435+
Returns:
436+
bool: True if mount successful, False otherwise
437+
"""
438+
try:
439+
logger.info(f" Mounting volume at '{mountpoint}'...")
440+
441+
# Check if we need sudo
442+
needs_sudo = not os.access('/etc', os.W_OK)
443+
444+
if needs_sudo:
445+
result = subprocess.run(['sudo', 'mount', mountpoint],
446+
capture_output=True, text=True)
447+
else:
448+
result = subprocess.run(['mount', mountpoint],
449+
capture_output=True, text=True)
450+
451+
if result.returncode == 0:
452+
logger.info(f" Successfully mounted volume at '{mountpoint}'")
453+
return True
454+
else:
455+
error_msg = result.stderr.strip()
456+
logger.info(f" Warning: Failed to mount volume: {error_msg}")
457+
logger.info(f" You can manually mount it later with: sudo mount {mountpoint}")
458+
return False
459+
460+
except Exception as e:
461+
logger.info(f" Warning: Error mounting volume: {e}")
462+
logger.info(f" You can manually mount it later with: sudo mount {mountpoint}")
463+
return False
464+
395465
def _get_expected_nfs_target(self, volume_name: str) -> str:
396466
"""Get the expected NFS target for a volume."""
397467
try:

netapp_dataops_traditional/netapp_dataops/config/manager.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,35 @@ def create_interactive_config(self) -> NetAppDataOpsConfig:
126126
dataset_manager_config = None
127127
if PromptUtils.prompt_yes_no("Do you intend to use the toolkit's Dataset Manager functionality?"):
128128
logger.info("\nDataset Manager Configuration:")
129+
130+
# IMPORTANT: Save base ONTAP config to disk BEFORE Dataset Manager setup
131+
# This allows Dataset Manager to connect to ONTAP and create volumes
132+
base_config = NetAppDataOpsConfig(
133+
ontap=ontap_config,
134+
s3=s3_config,
135+
cloud_sync=cloud_sync_config,
136+
dataset_manager=None # Will be added after setup
137+
)
138+
self.save_config(base_config)
139+
140+
# Now Dataset Manager can connect to ONTAP using the saved config
129141
dataset_manager_configurator = DatasetManagerConfigurator()
130142
dataset_manager_config = dataset_manager_configurator.configure_dataset_manager()
131143

132-
return NetAppDataOpsConfig(
144+
# Create final configuration object
145+
final_config = NetAppDataOpsConfig(
133146
ontap=ontap_config,
134147
s3=s3_config,
135148
cloud_sync=cloud_sync_config,
136149
dataset_manager=dataset_manager_config
137150
)
138151

152+
# Display configuration summary
153+
summary = self.get_config_summary(final_config)
154+
logger.info(summary)
155+
156+
return final_config
157+
139158
except Exception as e:
140159
raise ConfigCreationError(f"Failed to create configuration: {e}")
141160

@@ -338,6 +357,7 @@ def get_config_summary(self, config: NetAppDataOpsConfig) -> str:
338357
str: Configuration summary
339358
"""
340359
summary = []
360+
summary.append(f"\nCreated config file: {self.config_file}")
341361
summary.append("\nConfiguration Summary:")
342362
summary.append(f" ONTAP Host: {config.ontap.hostname}")
343363
summary.append(f" SVM: {config.ontap.svm}")

netapp_dataops_traditional/netapp_dataops/traditional/data_movement/s3_operations.py

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,49 +19,12 @@
1919
_print_invalid_config_error,
2020
deprecated
2121
)
22+
from ..core.connection import _instantiate_s3_session
23+
from ..core.config import _retrieve_s3_access_details
2224

2325
logger = setup_logger(__name__)
2426

2527

26-
def _instantiate_s3_session(s3Endpoint: str, s3AccessKeyId: str, s3SecretAccessKey: str, s3VerifySSLCert: bool, s3CACertBundle: str, print_output: bool = False):
27-
# Instantiate session
28-
session = boto3.session.Session(aws_access_key_id=s3AccessKeyId, aws_secret_access_key=s3SecretAccessKey)
29-
config = BotoConfig(signature_version='s3v4')
30-
31-
if s3VerifySSLCert:
32-
if s3CACertBundle:
33-
s3 = session.resource(service_name='s3', endpoint_url=s3Endpoint, verify=s3CACertBundle, config=config)
34-
else:
35-
s3 = session.resource(service_name='s3', endpoint_url=s3Endpoint, config=config)
36-
else:
37-
s3 = session.resource(service_name='s3', endpoint_url=s3Endpoint, verify=False, config=config)
38-
39-
return s3
40-
41-
42-
def _retrieve_s3_access_details(print_output: bool = False) -> Tuple[str, str, str, bool, str]:
43-
try:
44-
config = _retrieve_config(print_output=print_output)
45-
except InvalidConfigError:
46-
raise
47-
try:
48-
s3Endpoint = config["s3Endpoint"]
49-
s3AccessKeyId = config["s3AccessKeyId"]
50-
s3SecretAccessKeyBase64 = config["s3SecretAccessKey"]
51-
s3VerifySSLCert = config["s3VerifySSLCert"]
52-
s3CACertBundle = config["s3CACertBundle"]
53-
except KeyError:
54-
if print_output:
55-
_print_invalid_config_error()
56-
raise InvalidConfigError()
57-
58-
s3SecretAccessKeyBase64Bytes = s3SecretAccessKeyBase64.encode("ascii")
59-
s3SecretAccessKeyBytes = base64.b64decode(s3SecretAccessKeyBase64Bytes)
60-
s3SecretAccessKey = s3SecretAccessKeyBytes.decode("ascii")
61-
62-
return s3Endpoint, s3AccessKeyId, s3SecretAccessKey, s3VerifySSLCert, s3CACertBundle
63-
64-
6528
def _download_from_s3(s3Endpoint: str, s3AccessKeyId: str, s3SecretAccessKey: str, s3VerifySSLCert: bool,
6629
s3CACertBundle: str, s3Bucket: str, s3ObjectKey: str, localFile: str, print_output: bool = False):
6730
# Instantiate S3 session

0 commit comments

Comments
 (0)