Add support for loading resources from archive file

As Redfish Message registries can be reside inside archives,
added support to load resource from JSON files in archive.

Currently supporting only ZIP archives, support for other
archive types can be added as need arises or specification is
clarified. The Redfish specification does not detail
which types of archives need to be supported, but gives ZIP
as an example.

Change-Id: I3609df39c68f2149c1ff1a6818af7168bbd02df0
Story: 2001791
Task: 23062
This commit is contained in:
Aija Jaunteva 2018-08-06 15:49:05 +03:00
parent 28ee59fd79
commit 878a32e07f
4 changed files with 115 additions and 3 deletions

View File

@ -57,6 +57,10 @@ class InvalidParameterValueError(SushyError):
'Valid values are: %(valid_values)s')
class ArchiveParsingError(SushyError):
message = 'Failed parsing archive "%(path)s": %(error)s'
class HTTPError(SushyError):
"""Basic exception for HTTP errors"""

View File

@ -16,7 +16,10 @@
import abc
import collections
import copy
import io
import json
import logging
import zipfile
import six
@ -244,13 +247,69 @@ class MappedField(Field):
adapter=mapping.get)
@six.add_metaclass(abc.ABCMeta)
class AbstractJsonReader(object):
def set_connection(self, connector, path):
"""Sets mandatory connection parameters
:param connector: A Connector instance
:param path: path of the resource
"""
self._conn = connector
self._path = path
@abc.abstractmethod
def get_json(self):
"""Based on data source get data and parse to JSON"""
class JsonFileReader(AbstractJsonReader):
"""Gets the data from JSON file given by path"""
def get_json(self):
"""Gets JSON file from URI directly"""
return self._conn.get(path=self._path).json()
class JsonArchiveReader(AbstractJsonReader):
"""Gets the data from JSON file in archive"""
def __init__(self, archive_file):
"""Initializes the reader
:param archive_file: file name of JSON file in archive
"""
self._archive_file = archive_file
def get_json(self):
"""Gets JSON file from archive. Currently supporting ZIP only"""
data = self._conn.get(path=self._path)
if data.headers.get('content-type') == 'application/zip':
try:
archive = zipfile.ZipFile(io.BytesIO(data.content))
return json.loads(archive.read(self._archive_file)
.decode(encoding='utf-8'))
except (zipfile.BadZipfile, ValueError) as e:
raise exceptions.ArchiveParsingError(
path=self._path, error=e)
else:
LOG.error('Support for %(type)s not implemented',
{'type': data.headers['content-type']})
@six.add_metaclass(abc.ABCMeta)
class ResourceBase(object):
redfish_version = None
"""The Redfish version"""
def __init__(self, connector, path='', redfish_version=None):
def __init__(self,
connector,
path='',
redfish_version=None,
reader=JsonFileReader()):
"""A class representing the base of any Redfish resource
Invokes the ``refresh()`` method of resource for the first
@ -259,6 +318,8 @@ class ResourceBase(object):
:param path: sub-URI path to the resource.
:param redfish_version: The version of Redfish. Used to construct
the object according to schema of the given version.
:param reader: Reader to use to fetch JSON data. Defaults to
JsonFileReader
"""
self._conn = connector
self._path = path
@ -269,6 +330,9 @@ class ResourceBase(object):
# attribute values are fetched.
self._is_stale = True
reader.set_connection(connector, path)
self._reader = reader
self.refresh()
def _parse_attributes(self):
@ -299,7 +363,8 @@ class ResourceBase(object):
if not self._is_stale and not force:
return
self._json = self._conn.get(path=self._path).json()
self._json = self._reader.get_json()
LOG.debug('Received representation of %(type)s %(path)s: %(json)s',
{'type': self.__class__.__name__,
'path': self._path, 'json': self._json})

Binary file not shown.

View File

@ -14,13 +14,15 @@
# under the License.
import copy
import io
import mock
from six.moves import http_client
from sushy import exceptions
from sushy.resources import base as resource_base
from sushy.tests.unit import base
import zipfile
class BaseResource(resource_base.ResourceBase):
@ -59,6 +61,47 @@ class ResourceBaseTestCase(base.TestCase):
self.base_resource.invalidate(force_refresh=True)
self.conn.get.assert_called_once_with(path='/Foo')
def test_refresh_archive(self):
mock_response = mock.Mock(
headers={'content-type': 'application/zip'})
with open('sushy/tests/unit/json_samples/TestRegistry.zip', 'rb') as f:
mock_response.content = f.read()
self.conn.get.return_value = mock_response
resource = BaseResource(connector=self.conn,
path='/Foo',
redfish_version='1.0.2',
reader=resource_base.
JsonArchiveReader('Test.2.0.json'))
self.assertIsNotNone(resource._json)
self.assertEqual('Test.2.0.0', resource._json['Id'])
@mock.patch.object(resource_base, 'LOG', autospec=True)
def test_refresh_archive_not_implemented(self, mock_log):
mock_response = mock.Mock(
headers={'content-type': 'application/gzip'})
self.conn.get.return_value = mock_response
BaseResource(connector=self.conn,
path='/Foo',
redfish_version='1.0.2',
reader=resource_base.JsonArchiveReader('Test.2.0.json'))
mock_log.error.assert_called_once()
@mock.patch.object(io, 'BytesIO', autospec=True)
def test_refresh_archive_badzip_error(self, mock_io):
mock_response = mock.Mock(
headers={'content-type': 'application/zip'})
mock_io.side_effect = zipfile.BadZipfile('Something wrong')
self.conn.get.return_value = mock_response
self.assertRaises(exceptions.SushyError,
BaseResource, connector=self.conn,
path='/Foo',
redfish_version='1.0.2',
reader=resource_base.
JsonArchiveReader('Test.2.0.json'))
class TestResource(resource_base.ResourceBase):
"""A concrete Test Resource to test against"""