Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# PyPowerFlex Change Log

## Version 1.10.0 - released on 29/03/24
- Added support for retrieving all the firmware repository, validating, deploying, editing, adding nodes and deleting a resource group from PowerFlex Manager.

## Version 1.9.0 - released on 29/02/24
- Added support for retrieving managed devices, service templates and deployments from PowerFlex Manager.

Expand Down
4 changes: 3 additions & 1 deletion PyPowerFlex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class PowerFlexClient:
'replication_pair',
'service_template',
'managed_device',
'deployment'
'deployment',
'firmware_repository'
)

def __init__(self,
Expand Down Expand Up @@ -98,6 +99,7 @@ def initialize(self):
self.__add_storage_entity('service_template', objects.ServiceTemplate)
self.__add_storage_entity('managed_device', objects.ManagedDevice)
self.__add_storage_entity('deployment', objects.Deployment)
self.__add_storage_entity('firmware_repository', objects.FirmwareRepository)
utils.init_logger(self.configuration.log_level)
if version.parse(self.system.api_version()) < version.Version('3.0'):
raise exceptions.PowerFlexClientException(
Expand Down
58 changes: 31 additions & 27 deletions PyPowerFlex/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

class Request:
GET = "get"
POST = "post"
PUT = "put"
DELETE = "delete"

def __init__(self, token, configuration):
self.token = token
Expand Down Expand Up @@ -67,39 +70,39 @@ def get_auth_headers(self, request_type=None):
return {'Authorization': 'Bearer {0}'.format(self.token.get()),
'content-type': 'application/json'}

def send_get_request(self, url, **url_params):
request_url = self.base_url + url.format(**url_params)
def send_request(self, method, url, params=None, **url_params):
params = params or {}
request_url = f"{self.base_url}{url.format(**url_params)}"
version = self.login()
request_params = {'url': request_url,
'headers': self.get_auth_headers(request_type=self.GET),
'verify': self.verify_certificate,
'timeout': self.configuration.timeout}
request_params = {
'headers': self.get_auth_headers(method),
'verify': self.verify_certificate,
'timeout': self.configuration.timeout
}
if utils.is_version_3(version):
request_params['auth'] = (self.configuration.username,
self.token.get())
request_params['auth'] = (self.configuration.username, self.token.get())
request_params['headers'] = None
r = requests.get(**request_params)

if method in [self.PUT, self.POST]:
request_params['data'] = utils.prepare_params(params)
response = requests.request(method, request_url, **request_params)
self.logout(version)
response = r.json()
return r, response
return response

def send_get_request(self, url, params=None, **url_params):
response = self.send_request(self.GET, url, params, **url_params)
return response, response.json()

def send_post_request(self, url, params=None, **url_params):
if params is None:
params = dict()
version = self.login()
request_url = self.base_url + url.format(**url_params)
r = requests.post(request_url,
auth=(
self.configuration.username,
self.token.get()
),
headers=self.headers,
data=utils.prepare_params(params),
verify=self.verify_certificate,
timeout=self.configuration.timeout)
response = r.json()
self.logout(version)
return r, response
response = self.send_request(self.POST, url, params, **url_params)
return response, response.json()

def send_put_request(self, url, params=None, **url_params):
response = self.send_request(self.PUT, url, params, **url_params)
return response, response.json()

def send_delete_request(self, url, params=None, **url_params):
return self.send_request(self.DELETE, url, params, **url_params)

def send_mdm_cluster_post_request(self, url, params=None, **url_params):
if params is None:
Expand Down Expand Up @@ -234,6 +237,7 @@ class EntityRequest(Request):
service_template_url = '/V1/ServiceTemplate'
managed_device_url = '/V1/ManagedDevice'
deployment_url = '/V1/Deployment'
firmware_repository_url = '/V1/FirmwareRepository'
entity_name = None

@property
Expand Down
2 changes: 2 additions & 0 deletions PyPowerFlex/objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from PyPowerFlex.objects.service_template import ServiceTemplate
from PyPowerFlex.objects.managed_device import ManagedDevice
from PyPowerFlex.objects.deployment import Deployment
from PyPowerFlex.objects.firmware_repository import FirmwareRepository


__all__ = [
Expand All @@ -48,4 +49,5 @@
'ServiceTemplate',
'ManagedDevice',
'Deployment',
'FirmwareRepository',
]
90 changes: 90 additions & 0 deletions PyPowerFlex/objects/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,93 @@ def get(self, filters=None, full=None, include_devices=None, include_template=No
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)
return response

def get_by_id(self, deployment_id):
"""
Retrieve Deployment for specified ID.
:param deployment_id: Deployment ID.
:return: A dictionary containing the retrieved Deployment.
"""
r, response = self.send_get_request(f'{self.deployment_url}/{deployment_id}')
if r.status_code != requests.codes.ok:
msg = (f'Failed to retrieve deployment by id {deployment_id}. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)
return response

def validate(self, rg_data):
"""
Validates a new deployment.
Args:
rg_data (dict): The resource group data to be deployed.
Returns:
dict: The response from the deployment API.
Raises:
PowerFlexClientException: If the deployment fails.
"""
r, response = self.send_post_request(f'{self.deployment_url}/validate', rg_data)
if r.status_code != requests.codes.ok:
msg = (f'Failed to validate the deployment. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)

return response

def create(self, rg_data):
"""
Creates a new deployment.
Args:
rg_data (dict): The resource group data to be deployed.
Returns:
dict: The response from the deployment API.
Raises:
PowerFlexClientException: If the deployment fails.
"""
r, response = self.send_post_request(self.deployment_url, rg_data)
if r.status_code != requests.codes.ok:
msg = (f'Failed to create a new deployment. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)

return response

def edit(self, deployment_id, rg_data):
"""
Edit a deployment with the given ID using the provided data.
Args:
deployment_id (str): The ID of the deployment to edit.
rg_data (dict): The data to use for editing the deployment.
Returns:
dict: The response from the API.
Raises:
PowerFlexClientException: If the request fails.
"""
request_url = f'{self.deployment_url}/{deployment_id}'
r, response = self.send_put_request(request_url, rg_data)

if r.status_code != requests.codes.ok:
msg = (f'Failed to edit the deployment. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)

return response

def delete(self, deployment_id):
"""
Deletes a deployment with the given ID.
Args:
deployment_id (str): The ID of the deployment to delete.
Returns:
str: The response from the delete request.
Raises:
exceptions.PowerFlexClientException: If the delete request fails.
"""
request_url = f'{self.deployment_url}/{deployment_id}'
response = self.send_delete_request(request_url)

if response.status_code != requests.codes.no_content:
msg = (f'Failed to delete deployment. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)

return response
51 changes: 51 additions & 0 deletions PyPowerFlex/objects/firmware_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright (c) 2024 Dell Inc. or its subsidiaries.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import logging
import requests
from PyPowerFlex import base_client
from PyPowerFlex import exceptions
from PyPowerFlex import utils
LOG = logging.getLogger(__name__)


class FirmwareRepository(base_client.EntityRequest):
def get(self, filters=None, limit=None, offset=None, sort=None, related=False, bundles=False, components=False):
"""
Retrieve all firmware repository with filter, sort, pagination
:param filters: (Optional) The filters to apply to the results.
:param limit: (Optional) Page limit.
:param offset: (Optional) Pagination offset.
:param sort: (Optional) The field to sort the results by.
:param related: Whether to include related entities in the response.
:param bundles: Whether to include bundles in the response.
:param components: Whether to include components in the response.
:return: A list of dictionary containing the retrieved firmware repository.
"""
params = dict(
filter=filters,
sort=sort,
offset=offset,
limit=limit,
related=related,
bundles=bundles,
components=components
)
r, response = self.send_get_request(utils.build_uri_with_params(self.firmware_repository_url, **params))
if r.status_code != requests.codes.ok:
msg = (f'Failed to retrieve firmware repository. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)
return response
17 changes: 17 additions & 0 deletions PyPowerFlex/objects/service_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,20 @@ def get(self, filters=None, full=None, limit=None, offset=None, sort=None, inclu
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)
return response

def get_by_id(self, service_template_id, for_deployment=False):
"""
Retrieve a Service Template by its ID.
:param service_template_id: The ID of the Service Template to retrieve.
:param for_deployment: (Optional) Whether to retrieve the Service Template for deployment.
:return: A dictionary containing the retrieved Service Template.
"""
url = f'{self.service_template_url}/{service_template_id}'
if for_deployment:
url += '?forDeployment=true'
r, response = self.send_get_request(url)
if r.status_code != requests.codes.ok:
msg = (f'Failed to retrieve service template by id {service_template_id}. Error: {response}')
LOG.error(msg)
raise exceptions.PowerFlexClientException(msg)
return response
3 changes: 3 additions & 0 deletions PyPowerFlex/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ def prepare_params(params, dump=True):
:return: prepared parameters
"""

if not isinstance(params, dict):
return params

prepared = dict()
for name, value in params.items():
if value is not None:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ python setup.py install
* ManagedDevice
* Deployment
* ServiceTemplate
* FirmwareRepository

#### Initialize PowerFlex client

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

setup(
name='PyPowerFlex',
version='1.9.0',
version='1.10.0',
description='Python library for Dell PowerFlex',
author='Ansible Team at Dell',
author_email='ansible.team@dell.com',
Expand Down
17 changes: 12 additions & 5 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ class MockResponse(requests.Response):

def __init__(self, content, status_code=200):
super(MockResponse, self).__init__()

self._content = content
self.request = mock.MagicMock()
self.status_code = status_code
Expand Down Expand Up @@ -103,6 +102,7 @@ def setUp(self):
self.username,
self.password,
log_level=logging.DEBUG)
requests.request = self.get_mock_response
self.get_mock = self.mock_object(requests,
'get',
side_effect=self.get_mock_response)
Expand All @@ -117,7 +117,6 @@ def mock_object(self, obj, attr_name, *args, **kwargs):
Mocks the specified objects attribute with the given value.
Automatically performs 'addCleanup' for the mock.
"""

patcher = mock.patch.object(obj, attr_name, *args, **kwargs)
result = patcher.start()
self.addCleanup(patcher.stop)
Expand All @@ -131,10 +130,11 @@ def http_response_mode(self, mode):
yield
self.__http_response_mode = previous_response_mode

def get_mock_response(self, url, mode=None, *args, **kwargs):
def get_mock_response(self, url, request_url=None, mode=None, *args, **kwargs):
if mode is None:
mode = self.__http_response_mode
api_path = url.split('/api')[1]

api_path = url.split('/api')[1] if ('/api' in url) else request_url.split('/api')[1]
try:
if api_path == "/login":
response = self.RESPONSE_MODE.Valid[0]
Expand All @@ -155,7 +155,14 @@ def get_mock_response(self, url, mode=None, *args, **kwargs):
)
)
if not isinstance(response, MockResponse):
response = MockResponse(response, 200)
response = self._get_mock_response(response)

response.request.url = url
response.request.body = kwargs.get('data')
return response

def _get_mock_response(self, response):
if "204" in str(response):
return MockResponse(response, 204)
else:
return MockResponse(response, 200)
Loading