From 55b8b69ddf766349cae359b64a8e5f9fde99cf1c Mon Sep 17 00:00:00 2001 From: Felipe Garcia Bulsoni Date: Tue, 12 Sep 2017 14:41:24 -0300 Subject: [PATCH 1/3] Added new resource "index resource" and unit tests --- CHANGELOG.md | 4 +- examples/index_resources.py | 56 ++++++ hpOneView/oneview_client.py | 14 ++ hpOneView/resources/search/index_resources.py | 168 ++++++++++++++++++ .../resources/search/test_index_resources.py | 91 ++++++++++ 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 examples/index_resources.py create mode 100644 hpOneView/resources/search/index_resources.py create mode 100644 tests/unit/resources/search/test_index_resources.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f9040a..09a8be93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # v4.1.0 #### New Resources: - - Appliance node information +- Appliance node information +- Index resource #### Bug fixes & Enhancements - [#309](https://github.com/HewlettPackard/python-hpOneView/issues/309) HPOneViewException not raised when connection with paused VM fails +- [#312](https://github.com/HewlettPackard/python-hpOneView/issues/312) Could not find mappings for OneView's Index Resources # v4.0.0 #### Notes diff --git a/examples/index_resources.py b/examples/index_resources.py new file mode 100644 index 00000000..8138c941 --- /dev/null +++ b/examples/index_resources.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +### +# (C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +### +from pprint import pprint + +from hpOneView.oneview_client import OneViewClient +from config_loader import try_load_from_file + +config = { + "ip": "", + "credentials": { + "userName": "", + "password": "" + } +} + +attribute = 'Model' +category = 'server-hardware' + + +# Try load config from a file (if there is a config file) +config = try_load_from_file(config) + +oneview_client = OneViewClient(config) + +print('\nGetting all index resources:') +index_resources = oneview_client.index_resources.get_all() +pprint(index_resources) + +sh = oneview_client.server_hardware.get_all()[0] +print('\nGetting index resource for server hardware with uri "{0}":'.format(sh['uri'])) +index_resource = oneview_client.index_resources.get(sh['uri']) +pprint(index_resource) + +print('\nGetting aggregated index resources with attribute: "{0}" and category: "{1}"'.format(attribute, category)) +index_resources = oneview_client.index_resources.get_aggregated(attribute, category) +pprint(index_resources) diff --git a/hpOneView/oneview_client.py b/hpOneView/oneview_client.py index aebd17f2..d9bcc556 100644 --- a/hpOneView/oneview_client.py +++ b/hpOneView/oneview_client.py @@ -96,6 +96,7 @@ from hpOneView.resources.networking.uplink_sets import UplinkSets from hpOneView.resources.servers.migratable_vc_domains import MigratableVcDomains from hpOneView.resources.networking.sas_logical_interconnect_groups import SasLogicalInterconnectGroups +from hpOneView.resources.search.index_resources import IndexResources from hpOneView.resources.search.labels import Labels from hpOneView.resources.activity.alerts import Alerts from hpOneView.resources.activity.events import Events @@ -175,6 +176,7 @@ def __init__(self, config): self.__managed_sans = None self.__migratable_vc_domains = None self.__sas_interconnects = None + self.__index_resources = None self.__labels = None self.__sas_logical_interconnect_groups = None self.__alerts = None @@ -1019,6 +1021,18 @@ def labels(self): self.__labels = Labels(self.__connection) return self.__labels + @property + def index_resources(self): + """ + Gets the Index Resources API client. + + Returns: + IndexResources: + """ + if not self.__index_resources: + self.__index_resources = IndexResources(self.__connection) + return self.__index_resources + @property def alerts(self): """ diff --git a/hpOneView/resources/search/index_resources.py b/hpOneView/resources/search/index_resources.py new file mode 100644 index 00000000..17c947b4 --- /dev/null +++ b/hpOneView/resources/search/index_resources.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +### +# (C) Copyright (2017) Hewlett Packard Enterprise Development LP +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +### + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from future import standard_library + +standard_library.install_aliases() + +from hpOneView.resources.resource import ResourceClient +from urllib.parse import quote + + +class IndexResources(object): + """ + Index Resources API client. + + """ + + URI = '/rest/index/resources' + + def __init__(self, con): + self._connection = con + self._client = ResourceClient(con, self.URI) + + def get_all(self, category='', count=-1, fields='', filter='', padding=0, query='', reference_uri='', + sort='', start=0, user_query='', view=''): + """ + Gets a list of index resources based on optional sorting and filtering and is constrained by start + and count parameters. + + Args: + category (str or list): + Category of resources. Multiple Category parameters are applied with OR condition. + count (int): + The number of resources to return. A count of -1 requests all items. + The actual number of items in the response might differ from the requested + count if the sum of start and count exceeds the total number of items. + fields (str): + Specifies which fields should be returned in the result set. + filter (list or str): + A general filter/query string to narrow the list of items returned. The + default is no filter; all resources are returned. + padding (int): + Number of resources to be returned before the reference URI resource. + query (str): + A general query string to narrow the list of resources returned. + The default is no query - all resources are returned. + reference_uri (str): + Load one page of resources, pagination is applied with reference to referenceUri provided. + sort (str): + The sort order of the returned data set. By default, the sort order is based + on create time with the oldest entry first. + start (int): + The first item to return, using 0-based indexing. + If not specified, the default is 0 - start with the first available item. + user_query (str): + Free text Query string to search the resources. This will match the string in any field that is indexed. + view (str): + Return a specific subset of the attributes of the resource or collection, by specifying the name of a predefined view. + + Returns: + list: A list of index resources. + """ + uri = self.URI + '?' + + uri += self.__list_or_str_to_query(category, 'category') + uri += self.__list_or_str_to_query(count, 'count') + uri += self.__list_or_str_to_query(fields, 'fields') + uri += self.__list_or_str_to_query(filter, 'filter') + uri += self.__list_or_str_to_query(padding, 'padding') + uri += self.__list_or_str_to_query(query, 'query') + uri += self.__list_or_str_to_query(reference_uri, 'referenceUri') + uri += self.__list_or_str_to_query(sort, 'sort') + uri += self.__list_or_str_to_query(start, 'start') + uri += self.__list_or_str_to_query(user_query, 'userQuery') + uri += self.__list_or_str_to_query(view, 'view') + + response = self._client.get(uri) + + if response and 'members' in response and response['members']: + return response['members'] + else: + return [] + + def get(self, uri): + """ + Gets an index resource by URI. + + Args: + uri: The resource URI. + + Returns: + dict: The index resource. + """ + uri = self.URI + uri + return self._client.get(uri) + + def get_aggregated(self, attribute, category, child_limit=6, filter='', query='', user_query=''): + """ + Gets a list of index resources based on optional sorting and filtering and is constrained by start + and count parameters. + + Args: + attribute (list or str): + Attribute to pass in as query filter. + category (str): + Category of resources. Multiple Category parameters are applied with an OR condition. + child_limit (int): + Number of resources to be retrieved. Default=6. + filter (list or str): + A general filter/query string to narrow the list of items returned. The + default is no filter; all resources are returned. + query (str): + A general query string to narrow the list of resources returned. + The default is no query - all resources are returned. + user_query (str): + Free text Query string to search the resources. + This will match the string in any field that is indexed. + + Returns: + list: An aggregated list of index resources. + """ + uri = self.URI + '/aggregated?' + + # Add attribute to query + uri += self.__list_or_str_to_query(attribute, 'attribute') + uri += self.__list_or_str_to_query(category, 'category') + uri += self.__list_or_str_to_query(child_limit, 'childLimit') + uri += self.__list_or_str_to_query(filter, 'filter') + uri += self.__list_or_str_to_query(query, 'query') + uri += self.__list_or_str_to_query(user_query, 'userQuery') + + return self._client.get(uri) + + def __list_or_str_to_query(self, list_or_str, field_name): + formated_query = '' + if list_or_str: + if isinstance(list_or_str, list): + for f in list_or_str: + formated_query = formated_query + "&{0}=".format(field_name) + ''.join(quote(str(f))) + else: + formated_query = "&{0}=".format(field_name) + ''.join(quote(str(list_or_str))) + + return formated_query diff --git a/tests/unit/resources/search/test_index_resources.py b/tests/unit/resources/search/test_index_resources.py new file mode 100644 index 00000000..0dbf2b6d --- /dev/null +++ b/tests/unit/resources/search/test_index_resources.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +### +# (C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +### + +import unittest + +import mock + +from hpOneView.connection import connection +from hpOneView.resources.search.index_resources import IndexResources +from hpOneView.resources.resource import ResourceClient + + +class IndexResourcesTest(unittest.TestCase): + + INDEX_RESOURCE = dict( + uri="/rest/index/resources/rest/resource/uri", + resourceUri="/rest/resource/uri", + type="IndexResourceV300", + category="the-resource-category", + created="2014-03-31T02:08:27.884Z", + modified="2014-03-31T02:08:27.884Z", + eTag=None, + members=[{'name': 'sh1'}, {'name': 'sh2'}] + ) + + def return_index(self): + return self.INDEX_RESOURCE + + def setUp(self): + self.host = '127.0.0.1' + self.connection = connection(self.host) + self._resource = IndexResources(self.connection) + + @mock.patch.object(ResourceClient, 'get', return_value=dict(members='test')) + def test_get_all_called_once(self, mock_get_all): + filter = 'name=TestName' + sort = 'name:ascending' + + expected_uri = '/rest/index/resources?&count=500&filter=name%3DTestName&sort=name%3Aascending&start=2' + + self._resource.get_all(start=2, count=500, filter=filter, sort=sort) + mock_get_all.assert_called_once_with(expected_uri) + + @mock.patch.object(ResourceClient, 'get') + def test_get_all_called_once_without_results(self, mock_get_all): + filter = 'name=TestName' + sort = 'name:ascending' + + expected_uri = '/rest/index/resources?&count=500&filter=name%3DTestName&sort=name%3Aascending&start=2' + + self._resource.get_all(start=2, count=500, filter=filter, sort=sort) + mock_get_all.assert_called_once_with(expected_uri) + + @mock.patch.object(ResourceClient, 'get') + def test_get_called_once(self, mock_get): + index_uri = "/rest/server-hardwares/fake" + expected_call_uri = "/rest/index/resources/rest/server-hardwares/fake" + self._resource.get(index_uri) + mock_get.assert_called_once_with(expected_call_uri) + + @mock.patch.object(ResourceClient, 'get') + def test_get_aggregated_called_once(self, mock_get_aggregated): + + expected_uri = '/rest/index/resources/aggregated?&attribute=Model&attribute=State&category=server-hardware&childLimit=6' + + self._resource.get_aggregated(['Model', 'State'], 'server-hardware') + mock_get_aggregated.assert_called_once_with(expected_uri) + + +if __name__ == '__main__': + unittest.main() From 805be53950c8f90f20981863e4bb8d8fc5cd50bc Mon Sep 17 00:00:00 2001 From: Felipe Garcia Bulsoni Date: Tue, 12 Sep 2017 16:38:54 -0300 Subject: [PATCH 2/3] Removed first & from queries and unecessary join, increased line length --- examples/index_resources.py | 1 - hpOneView/resources/search/index_resources.py | 7 +++++-- tests/unit/resources/search/test_index_resources.py | 6 +++--- tox.ini | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/index_resources.py b/examples/index_resources.py index 8138c941..070bc8d6 100644 --- a/examples/index_resources.py +++ b/examples/index_resources.py @@ -36,7 +36,6 @@ attribute = 'Model' category = 'server-hardware' - # Try load config from a file (if there is a config file) config = try_load_from_file(config) diff --git a/hpOneView/resources/search/index_resources.py b/hpOneView/resources/search/index_resources.py index 17c947b4..4b9fb30f 100644 --- a/hpOneView/resources/search/index_resources.py +++ b/hpOneView/resources/search/index_resources.py @@ -99,6 +99,8 @@ def get_all(self, category='', count=-1, fields='', filter='', padding=0, query= uri += self.__list_or_str_to_query(user_query, 'userQuery') uri += self.__list_or_str_to_query(view, 'view') + uri = uri.replace('?&', '?') + response = self._client.get(uri) if response and 'members' in response and response['members']: @@ -154,6 +156,8 @@ def get_aggregated(self, attribute, category, child_limit=6, filter='', query='' uri += self.__list_or_str_to_query(query, 'query') uri += self.__list_or_str_to_query(user_query, 'userQuery') + uri = uri.replace('?&', '?') + return self._client.get(uri) def __list_or_str_to_query(self, list_or_str, field_name): @@ -163,6 +167,5 @@ def __list_or_str_to_query(self, list_or_str, field_name): for f in list_or_str: formated_query = formated_query + "&{0}=".format(field_name) + ''.join(quote(str(f))) else: - formated_query = "&{0}=".format(field_name) + ''.join(quote(str(list_or_str))) - + formated_query = "&{0}=".format(field_name) + str(list_or_str) return formated_query diff --git a/tests/unit/resources/search/test_index_resources.py b/tests/unit/resources/search/test_index_resources.py index 0dbf2b6d..b842fb36 100644 --- a/tests/unit/resources/search/test_index_resources.py +++ b/tests/unit/resources/search/test_index_resources.py @@ -56,7 +56,7 @@ def test_get_all_called_once(self, mock_get_all): filter = 'name=TestName' sort = 'name:ascending' - expected_uri = '/rest/index/resources?&count=500&filter=name%3DTestName&sort=name%3Aascending&start=2' + expected_uri = '/rest/index/resources?count=500&filter=name=TestName&sort=name:ascending&start=2' self._resource.get_all(start=2, count=500, filter=filter, sort=sort) mock_get_all.assert_called_once_with(expected_uri) @@ -66,7 +66,7 @@ def test_get_all_called_once_without_results(self, mock_get_all): filter = 'name=TestName' sort = 'name:ascending' - expected_uri = '/rest/index/resources?&count=500&filter=name%3DTestName&sort=name%3Aascending&start=2' + expected_uri = '/rest/index/resources?count=500&filter=name=TestName&sort=name:ascending&start=2' self._resource.get_all(start=2, count=500, filter=filter, sort=sort) mock_get_all.assert_called_once_with(expected_uri) @@ -81,7 +81,7 @@ def test_get_called_once(self, mock_get): @mock.patch.object(ResourceClient, 'get') def test_get_aggregated_called_once(self, mock_get_aggregated): - expected_uri = '/rest/index/resources/aggregated?&attribute=Model&attribute=State&category=server-hardware&childLimit=6' + expected_uri = '/rest/index/resources/aggregated?attribute=Model&attribute=State&category=server-hardware&childLimit=6' self._resource.get_aggregated(['Model', 'State'], 'server-hardware') mock_get_aggregated.assert_called_once_with(expected_uri) diff --git a/tox.ini b/tox.ini index e1716ab2..8358a31e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ skip_missing_interpreters = true [flake8] ignore = E402 -max-line-length = 120 +max-line-length = 140 exclude = tests.py, hpOneView/__init__.py, examples/scmb/, examples/scripts max-complexity = 14 From 570593cd09a547de10f89555a20ad01661bedd90 Mon Sep 17 00:00:00 2001 From: Felipe Garcia Bulsoni Date: Wed, 13 Sep 2017 10:28:35 -0300 Subject: [PATCH 3/3] Bumped up SDK version for release of index resource --- CHANGELOG.md | 10 +++++++--- hpOneView/__init__.py | 2 +- setup.py | 4 ++-- tests/unit/test_oneview_client.py | 8 ++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a8be93..7612d300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,16 @@ -# v4.1.0 +# v4.2.0 +#### New Resources: +- Index resource +#### Bug fixes & Enhancements +- [#312](https://github.com/HewlettPackard/python-hpOneView/issues/312) Could not find mappings for OneView's Index Resources + +# v4.1.0 #### New Resources: - Appliance node information -- Index resource #### Bug fixes & Enhancements - [#309](https://github.com/HewlettPackard/python-hpOneView/issues/309) HPOneViewException not raised when connection with paused VM fails -- [#312](https://github.com/HewlettPackard/python-hpOneView/issues/312) Could not find mappings for OneView's Index Resources # v4.0.0 #### Notes diff --git a/hpOneView/__init__.py b/hpOneView/__init__.py index 939c0e8b..5bec3e15 100644 --- a/hpOneView/__init__.py +++ b/hpOneView/__init__.py @@ -14,7 +14,7 @@ standard_library.install_aliases() __title__ = 'hpOneView' -__version__ = '3.3.0' +__version__ = '4.2.0' __copyright__ = '(C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP' __license__ = 'MIT' diff --git a/setup.py b/setup.py index 3fe21c28..fc6a438f 100644 --- a/setup.py +++ b/setup.py @@ -26,10 +26,10 @@ from setuptools import setup setup(name='hpOneView', - version='4.1.0', + version='4.2.0', description='HPE OneView Python Library', url='https://github.com/HewlettPackard/python-hpOneView', - download_url="https://github.com/HewlettPackard/python-hpOneView/tarball/v4.1.0", + download_url="https://github.com/HewlettPackard/python-hpOneView/tarball/v4.2.0", author='Hewlett Packard Enterprise Development LP', author_email='oneview-pythonsdk@hpe.com', license='MIT', diff --git a/tests/unit/test_oneview_client.py b/tests/unit/test_oneview_client.py index 723981e1..f7279b03 100644 --- a/tests/unit/test_oneview_client.py +++ b/tests/unit/test_oneview_client.py @@ -67,6 +67,7 @@ from hpOneView.resources.storage.drive_enclosures import DriveEnclosures from hpOneView.resources.storage.sas_logical_jbod_attachments import SasLogicalJbodAttachments from hpOneView.resources.networking.internal_link_sets import InternalLinkSets +from hpOneView.resources.search.index_resources import IndexResources from hpOneView.resources.search.labels import Labels from hpOneView.resources.uncategorized.os_deployment_plans import OsDeploymentPlans from hpOneView.resources.uncategorized.os_deployment_servers import OsDeploymentServers @@ -693,6 +694,13 @@ def test_lazy_loading_internal_link_sets(self): internal_links = self._oneview.internal_link_sets self.assertEqual(internal_links, self._oneview.internal_link_sets) + def test_index_resources_has_right_type(self): + self.assertIsInstance(self._oneview.index_resources, IndexResources) + + def test_lazy_loading_index_resources(self): + index_resources = self._oneview.index_resources + self.assertEqual(index_resources, self._oneview.index_resources) + def test_labels_has_right_type(self): self.assertIsInstance(self._oneview.labels, Labels)