Skip to content

Commit

Permalink
Merge pull request #16 from dell/release_2.1.0
Browse files Browse the repository at this point in the history
Release 2.1.0.0 for PyPowerStore Python Library
  • Loading branch information
Jennifer-John authored Sep 28, 2023
2 parents f9e3bce + 4fcfa95 commit c3abb6d
Show file tree
Hide file tree
Showing 19 changed files with 351 additions and 19 deletions.
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# PyPowerStore Change Log

## Version 2.1.0.0 - released on 29/09/23
- Added support for cloning, refreshing, and restoring filesystem.
- Added support for creating and deleting NAS server.
- Added support for discovered appliances.

## Version 2.0.0.0 - released on 30/06/23
- Added configuration operations for storage containers.
- Added support for manual appliance selection to volume.
Expand Down
18 changes: 18 additions & 0 deletions ProgrammersGuideExamples/discovered_appliances_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2023, Dell Technologies

""" Discovered Appliance operations"""

from PyPowerStore import powerstore_conn

CONN = powerstore_conn.PowerStoreConn(username="<username>",
password="<password>",
server_ip="<IP>",
verify=False,
application_type="<Application>",
timeout=180.0)
print(CONN)

# Get Discovered appliances list
discovered_appliances_list = CONN.config_mgmt.get_discovered_appliances()
print(discovered_appliances_list)
16 changes: 16 additions & 0 deletions ProgrammersGuideExamples/nas_server_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@
'protection_policy_id': 'samplepolicyid'
}

CREATE_PARAMS = {
"name": "test server",
"current_unix_directory_service": "LDAP",
"default_unix_user": "test-user",
"is_username_translation_enabled": True,
"is_auto_user_mapping_enabled": True,
}

# create nasserver
NAS_SERVER = CONN.provisioning.create_nasserver(CREATE_PARAMS)
print(NAS_SERVER)

# Get nasserver list
NAS_SERVERS = CONN.provisioning.get_nas_servers(all_pages=True)
print(NAS_SERVERS)
Expand All @@ -34,3 +46,7 @@
MODIFY_NAS = CONN.provisioning.modify_nasserver(NAS_SERVER[0]['id'],
MODIFY_PARAMS)
print(MODIFY_NAS)

# delete nasserver
DELETE_NAS = CONN.provisioning.delete_nasserver(NAS_SERVER[0]['id'])
print(DELETE_NAS)
2 changes: 1 addition & 1 deletion PyPowerStore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"""__init__.py."""

__title__ = 'PyPowerStore'
__version__ = '2.0.0.0'
__version__ = '2.1.0.0'
__author__ = 'Dell Technologies or its subsidiaries'
__copyright__ = 'Copyright 2019 Dell Technologies'
73 changes: 64 additions & 9 deletions PyPowerStore/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,10 @@ def get_appliances(self, filter_dict=None, all_pages=False):
"Getting all appliances with filter: '%s' and all_pages: '%s'"
% (filter_dict, all_pages))
querystring = helpers.prepare_querystring(
constants.SELECT_ID_NAME_AND_MODEL, filter_dict)
constants.APPLIANCE_DETAILS_QUERY, filter_dict)
if helpers.is_foot_hill_prime_or_higher():
querystring = helpers.prepare_querystring(
constants.APPLIANCE_DETAILS_FHP_QUERY, filter_dict)
LOG.info("Querystring: '%s'" % querystring)
return self.config_client.request(
constants.GET,
Expand All @@ -1046,13 +1049,18 @@ def get_appliance_details(self, appliance_id):

LOG.info("Getting appliance details by ID: '%s'" % appliance_id)
querystring = constants.APPLIANCE_DETAILS_QUERY
if helpers.is_foot_hill_or_higher():
if helpers.is_foot_hill_prime_or_higher():
querystring = constants.APPLIANCE_DETAILS_FHP_QUERY
elif helpers.is_foot_hill_or_higher():
querystring = {
'select': 'id,name,service_tag,express_service_code,model,'
'drive_failure_tolerance_level,nodes,'
'ip_pool_addresses,veth_ports,maintenance_windows,'
'fc_ports,sas_ports,eth_ports,software_installed,'
'virtual_volumes,hardware,volumes'
'ip_pool_addresses(id,name),veth_ports(id,name),'
'maintenance_windows,fc_ports(id,name),'
'sas_ports(id,name),eth_ports(id,name),'
'software_installed(id,release_version),'
'virtual_volumes(id,name),hardware(id,name),'
'volumes(id,name)'
}
return self.config_client.request(
constants.GET,
Expand All @@ -1071,13 +1079,18 @@ def get_appliance_by_name(self, appliance_name):
"""
LOG.info("Getting appliance details by name: '%s'" % appliance_name)
querystring = constants.APPLIANCE_DETAILS_QUERY
if helpers.is_foot_hill_or_higher():
if helpers.is_foot_hill_prime_or_higher():
querystring = constants.APPLIANCE_DETAILS_FHP_QUERY
elif helpers.is_foot_hill_or_higher():
querystring = {
'select': 'id,name,service_tag,express_service_code,model,'
'drive_failure_tolerance_level,nodes,'
'ip_pool_addresses,veth_ports,maintenance_windows,'
'fc_ports,sas_ports,eth_ports,software_installed,'
'virtual_volumes,hardware,volumes'
'ip_pool_addresses(id,name),veth_ports(id,name),'
'maintenance_windows,fc_ports(id,name),'
'sas_ports(id,name),eth_ports(id,name),'
'software_installed(id,release_version),'
'virtual_volumes(id,name),hardware(id,name),'
'volumes(id,name)'
}

return self.config_client.request(
Expand All @@ -1086,6 +1099,48 @@ def get_appliance_by_name(self, appliance_name):
querystring=helpers.prepare_querystring(
querystring,
name=constants.EQUALS + appliance_name))

# Discovered Appliances methods
def get_discovered_appliances(self, filter_dict=None, all_pages=False):
"""
Get discovered appliances.
Parameters:
- filter_dict (dict): (optional) Filter details
- all_pages (bool): (optional) Indicates whether to return all
appliances or not
Returns:
- List of discovered appliances (list)[dict]
"""
LOG.info("Getting discovered appliances with filter: '%s' and "
"all_pages: '%s'",filter_dict, all_pages)

if all_pages:
raise ValueError("Pagination is not supported for discovered "
"appliances.")

querystring = constants.DISCOVERED_APPLIANCE_DETAILS_QUERY.copy()

if not filter_dict:
querystring = helpers.prepare_querystring(querystring)

LOG.info("Querystring without filters dict: '%s'", querystring)
return self.config_client.request(
constants.GET,
constants.GET_DISCOVERED_APPLIANCE_LIST_URL.format(self.server_ip),
querystring=querystring,
all_pages=False
)
resp = self.config_client.request(
constants.GET,
constants.GET_DISCOVERED_APPLIANCE_LIST_URL.format(self.server_ip),
querystring=querystring, all_pages=False)
filterable_keys = ['id', 'link_local_address', 'service_name', 'state',
'is_unified_capable', 'mode', 'is_local',
'management_service_ready', 'build_version',
'node_count', 'express_service_code']
return helpers.filtered_details(filterable_keys, filter_dict,
resp, 'discovered_appliances')
# Appliance operations end

# Certificate operations start
Expand Down
92 changes: 91 additions & 1 deletion PyPowerStore/provisioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,64 @@ def _prepare_restore_volume_payload(self, snap_id_to_restore_from=None,

return refresh_volume_dict

def clone_filesystem(self, filesystem_id, advance_parameters):
"""Clone a filesystem.
:param filesystem_id: The filesystem ID
:type filesystem_id: str
:param advance_parameters: Advance attributes
:type advance_parameters: str
:return: Unique identifier of the new instance created if success else raise exception
:rtype: dict
"""
LOG.info("Cloning the filesystem: '%s'" % filesystem_id)
payload = dict()

if advance_parameters:
for key, value in advance_parameters.items():
if key in constants.FILESYSTEM_PRIME and \
not helpers.is_foot_hill_prime_or_higher():
raise Exception( key + " is supported for PowerStore" \
" version 3.0.0.0 and above.")
payload[key] = value
return self.client.request(
constants.POST, constants.CLONE_FILESYSTEM_URL.format(
self.server_ip, filesystem_id),
payload)

def restore_filesystem(self, snapshot_id, backup_snap_name=None):
"""Restore a filesystem.
:param snapshot_id: Unique identifier of the file system snapshot.
:type snapshot_id: str
:param backup_snap_name: Name of the backup snap to be created before the restore operation occurs. If no name is specified, no backup copy will be made.
:type backup_snap_name: str
:return: Unique identifier of the backup snapshot set or None if backup_snap_name is None
if success else raise exception
:rtype: dict
"""
LOG.info("Restoring the filesystem from snapshot: '%s'" % snapshot_id)
payload = {}
if backup_snap_name is not None:
payload['copy_name'] = backup_snap_name
return self.client.request(
constants.POST, constants.RESTORE_FILESYSTEM_URL.format(
self.server_ip, snapshot_id),
payload)

def refresh_filesystem(self, snapshot_id):
"""Refresh a filesystem.
:param snapshot_id: Unique identifier of the file system snapshot.
:type snapshot_id: str
:return: None if success else raise exception.
:rtype: None
"""
LOG.info("Refreshing the filesystem from snapshot: '%s'" % snapshot_id)
return self.client.request(
constants.POST, constants.REFRESH_FILESYSTEM_URL.format(
self.server_ip, snapshot_id))

def add_protection_policy_for_volume(self, volume_id,
protection_policy_id):
"""Add protection policy for volume.
Expand Down Expand Up @@ -1508,7 +1566,7 @@ def get_cluster_list(self):
constants.GET_CLUSTER.format
(self.server_ip), payload=None,
querystring=constants.
SELECT_ID_AND_NAME)
CLUSTER_DETAILS_QUERY)

def get_host_volume_mapping(self, volume_id):
"""Get Host volume mapping details.
Expand Down Expand Up @@ -1596,6 +1654,25 @@ def get_nas_server_by_name(self, nas_server_name):
)
)

def create_nasserver(self, payload):
"""Create a NAS Server.
:param payload: The payload to create the NAS Server
:type payload: dict
:return: NAS server ID on success else raise exception
:rtype: dict
"""
LOG.info("Creating NAS server: '%s'" % payload.get('name'))
if 'protection-policy' in payload and \
not helpers.is_foot_hill_prime_or_higher():
raise Exception("Protection policy is supported for PowerStore" \
" version 3.0.0.0 and above.")
return self.client.request(
constants.POST,
constants.CREATE_NAS_SERVER_URL.format(self.server_ip),
payload=payload)


def modify_nasserver(self, nasserver_id, modify_parameters):
"""Modify NAS Server attributes.
Expand All @@ -1622,6 +1699,19 @@ def modify_nasserver(self, nasserver_id, modify_parameters):

raise ValueError("Nothing to modify")

def delete_nasserver(self, nasserver_id):
"""Delete a NAS Server.
:param nasserver_id: The ID of the NAS Server to delete
:type nasserver_id: str
:return: None on success else raise exception
:rtype: None
"""
LOG.info("Deleting NAS server: '%s'" % nasserver_id)
return self.client.request(
constants.DELETE,
constants.DELETE_NAS_SERVER_URL.format(self.server_ip, nasserver_id))

# NAS Server methods end

# File System Methods
Expand Down
2 changes: 2 additions & 0 deletions PyPowerStore/tests/unit_tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from PyPowerStore.tests.unit_tests.data.storage_container_data import StorageContainerData
from PyPowerStore.tests.unit_tests.data.storage_container_destination_data import StorageContainerDestinationData
from PyPowerStore.tests.unit_tests.data.replication_group_data import ReplicationGroupData
from PyPowerStore.tests.unit_tests.data.discovered_appliances import DiscoveredApplianceData
from unittest import mock

class TestBase(TestCase):
Expand All @@ -45,6 +46,7 @@ def setUp(self):
self.vcenter_data = VcenterData()
self.virtual_volume_data = VirtualVolumeData()
self.file_system_data = FileSystemData()
self.discovered_appliance_data = DiscoveredApplianceData()
self.conf = PowerStoreConfig()
self.mock_client = mock.patch('PyPowerStore.provisioning.Client',
new=MockClient)
Expand Down
28 changes: 28 additions & 0 deletions PyPowerStore/tests/unit_tests/data/discovered_appliances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class DiscoveredApplianceData:
discovered_appliance_id = '51d0dc86-f0e8-2fdb-81g0-5cd1hgfhhd1e'
discovered_appliance_list = [
{
"id": "51d0dc86-f0e8-2fdb-81g0-5cd1hgfhhd1e",
"link_local_address": "xx.xx.xx.xx",
"service_name": "appliance",
"service_tag": "appliance-tag",
"state": "Unconfigured",
"mode": "Unified",
"model": "appliance-model",
"express_service_code": "service-code",
"is_local": True,
"management_service_ready": True,
"software_version_compatibility": "Same",
"build_version": "build-version",
"build_id": "build-id",
"power_score": 0,
"node_count": 1,
"is_unified_capable": True,
"drive_failure_tolerance_level_and_availability": [
{
"level": "Single",
"availability": "Available"
}
]
}
]
24 changes: 24 additions & 0 deletions PyPowerStore/tests/unit_tests/entity/discovered_appliances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from PyPowerStore.tests.unit_tests.entity.base_abstract import Entity
from PyPowerStore.tests.unit_tests.data.discovered_appliances import DiscoveredApplianceData


class DiscoveredApplianceResponse(Entity):

def __init__(self, method, url, **kwargs):
self.method = method
self.url = url
self.kwargs = kwargs
self.discovered_appliance_data = DiscoveredApplianceData()
self.status_code = 200

def get_api_name(self):
if self.method == 'GET':
return self.get_discovered_appliances

def execute_api(self, api_name):
status_code, response = api_name()
return status_code, response

def get_discovered_appliances(self):
return self.status_code,\
self.discovered_appliance_data.discovered_appliance_list
10 changes: 10 additions & 0 deletions PyPowerStore/tests/unit_tests/entity/file_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def get_api_name(self):
elif self.method == 'POST':
if self.url.endswith('/snapshot'):
return self.create_filesystem_snapshot
elif self.url.endswith('/refresh'):
return self.refresh_filesystem
elif self.url.endswith('/restore'):
return self.restore_filesystem
return self.create_filesystem
elif self.method == 'PATCH':
return self.modify_fs
Expand Down Expand Up @@ -57,6 +61,12 @@ def get_snapshots_filesystem(self):
def modify_fs(self):
return 204, None

def restore_filesystem(self):
return 204, None

def refresh_filesystem(self):
return 204, None

def delete_fs(self):
if self.url.endswith('/file_system/{0}'.format(
self.data.invalid_fs_id)):
Expand Down
Loading

0 comments on commit c3abb6d

Please sign in to comment.