├── makeandinstallzip.bat
├── requirements.txt
├── tests
├── __init__.py
├── ris
│ ├── __init__.py
│ ├── test_rmc_helper.py
│ ├── test_ris.py
│ └── test_config.py
├── rest
│ ├── __init__.py
│ └── test_v1.py
└── discovery
│ ├── __init__.py
│ └── test_discovery.py
├── src
└── redfish
│ ├── discovery
│ ├── __init__.py
│ └── discovery.py
│ ├── messages
│ ├── __init__.py
│ └── messages.py
│ ├── rest
│ ├── __init__.py
│ └── v1.py
│ ├── __init__.py
│ └── ris
│ ├── __init__.py
│ ├── sharedtypes.py
│ ├── config.py
│ ├── ris.py
│ └── rmc_helper.py
├── .travis.yml
├── tox.ini
├── .project
├── .pydevproject
├── examples
├── discover.py
├── context_manager.py
├── quickstart.py
├── multipart_push.py
├── quickstart_rmc.py
└── http_adapter.py
├── AUTHORS.md
├── .gitignore
├── LICENSE.md
├── setup.py
├── CONTRIBUTING.md
├── .github
└── workflows
│ └── main.yml
├── CHANGELOG.md
└── README.rst
/makeandinstallzip.bat:
--------------------------------------------------------------------------------
1 | python setup.py sdist --formats=zip
2 | cd dist
3 | pip install --upgrade redfish-2.0.0.zip
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jsonpatch<=1.24 ; python_version == '3.4'
2 | jsonpatch ; python_version >= '3.5'
3 | jsonpath_rw
4 | jsonpointer
5 | requests
6 | requests-toolbelt
7 | requests-unixsocket
8 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
--------------------------------------------------------------------------------
/tests/ris/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
--------------------------------------------------------------------------------
/tests/rest/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
--------------------------------------------------------------------------------
/tests/discovery/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
--------------------------------------------------------------------------------
/src/redfish/discovery/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
--------------------------------------------------------------------------------
/src/redfish/messages/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 | from .messages import *
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: focal
2 | language: python
3 | cache:
4 | - pip
5 | python:
6 | - '3.8'
7 | - '3.9'
8 | - '3.11'
9 | before_install:
10 | - pip install -U pip
11 | - pip install -U setuptools
12 | - pip install -U wheel
13 | install:
14 | - pip install tox-travis .[devel]
15 | script:
16 | - tox
17 |
--------------------------------------------------------------------------------
/src/redfish/rest/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | """ Utilities to simplify interaction with Redfish data """
7 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py38,py39,py311
3 |
4 | [testenv]
5 | usedevelop = True
6 | install_command = pip install {opts} {packages}
7 | deps =
8 | coverage
9 | fixtures
10 | pytest
11 | -rrequirements.txt
12 | commands =
13 | pytest -v
14 |
15 | [testenv:pep8]
16 | basepython = python3
17 | deps = flake8
18 | commands = flake8 tests/ src/redfish/discovery
19 |
20 | [travis]
21 | python = 3.11: py311
22 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | redfish
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | /${PROJECT_DIR_NAME}/src
5 |
6 | python 2.7
7 | Default
8 |
9 |
--------------------------------------------------------------------------------
/examples/discover.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | import redfish
7 |
8 | # Invoke the discovery routine for SSDP and print the responses
9 | services = redfish.discover_ssdp()
10 | for service in services:
11 | print( '{}: {}'.format(service, services[service]))
12 |
--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
1 | # Original Contribution:
2 |
3 | * [Jack Garcia](//github.com/lumbajack) - HPE - Hewlett Packard Enterprise Restful API Group
4 | * [Matthew Kocurek](//github.com/Yergidy) - HPE - Hewlett Packard Enterprise Restful API Group
5 | * [Prithvi Subrahmanya](//github.com/PrithviBS) - HPE - Hewlett Packard Enterprise Restful API Group
6 |
7 | # Other Key Contributions:
8 |
9 | * For a list of people who have contributed to the codebase, see [GitHub's list of contributors](https://github.com/DMTF/python-redfish-library/contributors).*
10 |
--------------------------------------------------------------------------------
/examples/context_manager.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | import sys
7 | import redfish
8 |
9 | # When running remotely connect using the address, account name,
10 | # and password to send https requests
11 | login_host = "https://192.168.1.100"
12 | login_account = "admin"
13 | login_password = "password"
14 |
15 | ## Create a REDFISH object
16 | with redfish.redfish_client(base_url=login_host, username=login_account, password=login_password) as REDFISH_OBJ:
17 | # Do a GET on a given path
18 | response = REDFISH_OBJ.get("/redfish/v1/systems/1", None)
19 |
20 | # Print out the response
21 | sys.stdout.write("%s\n" % response)
22 |
--------------------------------------------------------------------------------
/src/redfish/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | """ Redfish restful library """
7 |
8 | __all__ = ['rest', 'ris', 'discovery', 'messages']
9 | __version__ = "3.3.4"
10 |
11 | from redfish.rest.v1 import redfish_client
12 | from redfish.rest.v1 import AuthMethod
13 | from redfish.discovery.discovery import discover_ssdp
14 | from redfish.messages import *
15 | import logging
16 |
17 | def redfish_logger(file_name, log_format, log_level=logging.ERROR):
18 | formatter = logging.Formatter(log_format)
19 | fh = logging.FileHandler(file_name)
20 | fh.setFormatter(formatter)
21 | logger = logging.getLogger(__name__)
22 | logger.addHandler(fh)
23 | logger.setLevel(log_level)
24 | return logger
25 |
--------------------------------------------------------------------------------
/src/redfish/ris/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """
8 | RIS implementation
9 | """
10 |
11 | from .sharedtypes import (
12 | JSONEncoder
13 | )
14 |
15 | from .ris import (
16 | RisMonolithMemberBase,
17 | RisMonolithMember_v1_0_0,
18 | RisMonolith_v1_0_0,
19 | RisMonolith,
20 | )
21 |
22 | from .rmc_helper import (
23 | UndefinedClientError,
24 | InstanceNotFoundError,
25 | CurrentlyLoggedInError,
26 | NothingSelectedError,
27 | NothingSelectedSetError,
28 | InvalidSelectionError,
29 | SessionExpired,
30 | RmcClient,
31 | RmcConfig,
32 | RmcCacheManager,
33 | RmcFileCacheManager,
34 | )
35 |
36 | from .rmc import (
37 | RmcApp
38 | )
39 |
--------------------------------------------------------------------------------
/tests/ris/test_rmc_helper.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | import unittest
7 | try:
8 | from unittest import mock
9 | except ImportError:
10 | import mock
11 |
12 | from redfish.ris import rmc_helper
13 |
14 |
15 | class RmcHelper(unittest.TestCase):
16 | def setUp(self):
17 | super(RmcHelper, self).setUp()
18 |
19 | @mock.patch('redfish.rest.v1.HttpClient')
20 | def test_get_cache_dirname(self, mock_http_client):
21 | url = 'http://example.com'
22 | helper = rmc_helper.RmcClient(url=url, username='oper', password='xyz')
23 | mock_http_client.return_value.get_base_url.return_value = url
24 | dir_name = helper.get_cache_dirname()
25 | self.assertEqual(dir_name, 'example.com/')
26 |
27 |
28 | if __name__ == '__main__':
29 | unittest.main()
30 |
--------------------------------------------------------------------------------
/tests/discovery/test_discovery.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- encoding: utf-8 -*-
7 | import unittest
8 |
9 | from redfish.discovery.discovery import FakeSocket
10 | from redfish.discovery.discovery import sanitize
11 |
12 | from io import BytesIO
13 |
14 |
15 | class TestFakeSocket(unittest.TestCase):
16 | def test_init(self):
17 | fake = FakeSocket(b"foo")
18 | self.assertTrue(isinstance(fake, FakeSocket))
19 | self.assertTrue(isinstance(fake._file, BytesIO))
20 |
21 |
22 | class TestDiscover(unittest.TestCase):
23 | def test_sanitize(self):
24 | self.assertEqual(sanitize(257, 1, 255), 255)
25 | self.assertEqual(sanitize(0, 1, 255), 1)
26 | self.assertEqual(sanitize(0, 1), 1)
27 | self.assertEqual(sanitize(2000, 1), 2000)
28 | self.assertEqual(sanitize(-1, 1), 1)
29 |
--------------------------------------------------------------------------------
/examples/quickstart.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | import sys
7 | import redfish
8 |
9 | # When running remotely connect using the address, account name,
10 | # and password to send https requests
11 | login_host = "https://192.168.1.100"
12 | login_account = "admin"
13 | login_password = "password"
14 |
15 | ## Create a REDFISH object
16 | REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account,
17 | password=login_password, default_prefix='/redfish/v1')
18 |
19 | # Login into the server and create a session
20 | REDFISH_OBJ.login(auth="session")
21 |
22 | # Do a GET on a given path
23 | response = REDFISH_OBJ.get("/redfish/v1/systems/1", None)
24 |
25 | # Print out the response
26 | sys.stdout.write("%s\n" % response)
27 |
28 | # Logout of the current session
29 | REDFISH_OBJ.logout()
--------------------------------------------------------------------------------
/tests/ris/test_ris.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- encoding: utf-8 -*-
7 | import unittest
8 |
9 | from redfish.ris import RisMonolithMember_v1_0_0
10 | from redfish.ris import RisMonolithMemberBase
11 | from redfish.ris.sharedtypes import Dictable
12 |
13 |
14 | class TestRisMonolithMemberBase(unittest.TestCase):
15 | def test_init(self):
16 | RisMonolithMemberBase()
17 | self.assertTrue(issubclass(RisMonolithMemberBase, Dictable))
18 |
19 |
20 | class TestRisMonolithMember_v1_0_0(unittest.TestCase):
21 | def test_init(self):
22 | with self.assertRaises(TypeError):
23 | RisMonolithMember_v1_0_0()
24 |
25 | RisMonolithMember_v1_0_0("test")
26 | self.assertTrue(
27 | issubclass(RisMonolithMember_v1_0_0, RisMonolithMemberBase)
28 | )
29 | self.assertTrue(issubclass(RisMonolithMember_v1_0_0, Dictable))
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *__pycache__*
4 | *.py[cod]
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # pyenv
34 | .python-version
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 |
56 | # Sphinx documentation
57 | docs/_build/
58 |
59 | # PyBuilder
60 | target/
61 |
62 | # Editors
63 | *.swa
64 | *.swp
65 | *~
66 |
--------------------------------------------------------------------------------
/src/redfish/ris/sharedtypes.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """ Shared types used in this module """
8 |
9 | #---------Imports---------
10 |
11 | import logging
12 | import jsonpatch
13 | from redfish.rest.v1 import JSONEncoder
14 |
15 | #---------End of imports---------
16 |
17 | #---------Debug logger---------
18 |
19 | LOGGER = logging.getLogger(__name__)
20 |
21 | #---------End of debug logger---------
22 |
23 | class JSONEncoder(JSONEncoder):
24 | """Custom JSONEncoder that understands our types"""
25 | def default(self, obj):
26 | """Set defaults
27 |
28 | :param obj: json object.
29 | :type obj: str.
30 |
31 | """
32 | if isinstance(obj, Dictable):
33 | return obj.to_dict()
34 | elif isinstance(obj, jsonpatch.JsonPatch):
35 | return obj.patch
36 | return super(JSONEncoder, self).default(obj)
37 |
38 | class Dictable(object):
39 | """A base class which adds the to_dict method used during json encoding"""
40 | def to_dict(self):
41 | """Overridable funciton"""
42 | raise RuntimeError("You must override this method in your derived"
43 | " class")
44 |
45 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2016-2024, Contributing Member(s) of Distributed Management Task
4 | Force, Inc.. All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation and/or
14 | other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its contributors
17 | may be used to endorse or promote products derived from this software without
18 | specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | from setuptools import setup, find_packages
7 | from codecs import open
8 | from os import path
9 |
10 | here = path.abspath(path.dirname(__file__))
11 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
12 | long_description = f.read()
13 |
14 | setup(name='redfish',
15 | version='3.3.4',
16 | description='Redfish Python Library',
17 | long_description=long_description,
18 | long_description_content_type='text/x-rst',
19 | author = 'DMTF, https://www.dmtf.org/standards/feedback',
20 | license='BSD 3-clause "New" or "Revised License"',
21 | classifiers=[
22 | 'Development Status :: 5 - Production/Stable',
23 | 'License :: OSI Approved :: BSD License',
24 | 'Programming Language :: Python :: 3',
25 | 'Topic :: Communications'
26 | ],
27 | keywords='Redfish',
28 | url='https://github.com/DMTF/python-redfish-library',
29 | packages=find_packages('src'),
30 | package_dir={'': 'src'},
31 | install_requires=[
32 | 'jsonpath_rw',
33 | 'jsonpointer',
34 | "requests",
35 | 'requests_toolbelt',
36 | 'requests-unixsocket'
37 | ],
38 | extras_require={
39 | ':python_version == "3.4"': [
40 | 'jsonpatch<=1.24'
41 | ],
42 | ':python_version >= "3.5"': [
43 | 'jsonpatch'
44 | ]
45 | })
46 |
--------------------------------------------------------------------------------
/examples/multipart_push.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | import sys
7 | import json
8 | import redfish
9 |
10 | # When running remotely connect using the address, account name,
11 | # and password to send https requests
12 | login_host = "https://192.168.1.100"
13 | login_account = "admin"
14 | login_password = "password"
15 |
16 | ## Create a REDFISH object
17 | REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account,
18 | password=login_password, default_prefix='/redfish/v1')
19 |
20 | # Login into the server and create a session
21 | REDFISH_OBJ.login(auth="session")
22 |
23 | # Format parts of the Update
24 | headers = {'Content-Type': 'multipart/form-data'}
25 | body = {}
26 | body['UpdateParameters'] = (None, json.dumps({'Targets': ['/redfish/v1/Managers/1'], 'Oem': {}}), 'application/json')
27 | body['UpdateFile'] = ('flash.bin', open('flash.bin', 'rb'), 'application/octet-stream')
28 |
29 | # The "OemXXX" part is optional in the specification
30 | # Must be formatted as 3-tuple:
31 | # ('filename' or None, content, content-type),
32 | body['OemXXX'] = (None, '{"test": "value"}', 'application/json')
33 | body['OemXXX'] = ('extra.bin', open('extra.txt', 'rb'), 'application/octet-stream')
34 | body['OemXXX'] = ('optional.txt', open('optional.txt', 'r').read(), 'text/plain')
35 |
36 | # Perform the POST operation
37 | response = REDFISH_OBJ.post('/redfish/v1/upload', body=body, headers=headers)
38 |
39 | # Print out the response
40 | sys.stdout.write("%s\n" % response)
41 |
42 | # Logout of the current session
43 | REDFISH_OBJ.logout()
44 |
--------------------------------------------------------------------------------
/examples/quickstart_rmc.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | import os
7 | import sys
8 | import json
9 | import logging
10 |
11 | from redfish import redfish_logger
12 | from redfish.ris import RmcApp, JSONEncoder
13 |
14 | # Config logger used by Restful library
15 | LOGGERFILE = "RedfishApiExamples.log"
16 | LOGGERFORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
17 | LOGGER = redfish_logger(LOGGERFILE, LOGGERFORMAT, logging.ERROR)
18 | LOGGER.info("Redfish API examples")
19 |
20 | # When running remotely connect using the address, account name,
21 | # and password to send https requests
22 | login_host = "https://192.168.1.100"
23 | login_account = "admin"
24 | login_password = "password"
25 |
26 | # Creating RMC object
27 | RMCOBJ = RmcApp([])
28 |
29 | # Create cache directory
30 | config_dir = r'C:\DATA\redfish'
31 | RMCOBJ.config.set_cachedir(os.path.join(config_dir, 'cache'))
32 | cachedir = RMCOBJ.config.get_cachedir()
33 |
34 | # If current cache exist try to log it out
35 | if os.path.isdir(cachedir):
36 | RMCOBJ.logout
37 |
38 |
39 | # Login into the server and create a session
40 | RMCOBJ.login(username=login_account, password=login_password, \
41 | base_url=login_host)
42 |
43 | # Select ComputerSystems
44 | RMCOBJ.select(['ComputerSystem.'])
45 |
46 | # Get selected type
47 | response = RMCOBJ.get()
48 |
49 | # Print out the response
50 | for item in response:
51 | sys.stdout.write(json.dumps(item, indent=2, cls=JSONEncoder))
52 | sys.stdout.write('\n')
53 |
54 | # Logout of the current session
55 | RMCOBJ.logout()
56 |
--------------------------------------------------------------------------------
/tests/ris/test_config.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- encoding: utf-8 -*-
7 | import tempfile
8 | import textwrap
9 | import unittest
10 |
11 | from redfish.ris.config import AutoConfigParser
12 |
13 |
14 | CONFIG = textwrap.dedent(
15 | """
16 | [DEFAULT]
17 | ServerAliveInterval = 45
18 | Compression = yes
19 | CompressionLevel = 9
20 | ForwardX11 = yes
21 |
22 | [bitbucket.org]
23 | User = hg
24 |
25 | [topsecret.server.com]
26 | Port = 50022
27 | ForwardX11 = no
28 | """
29 | )
30 |
31 |
32 | class TestAutoConfigParser(unittest.TestCase):
33 | def test_init(self):
34 | acp = AutoConfigParser()
35 | self.assertEqual(acp._configfile, None)
36 | with tempfile.TemporaryDirectory() as tmpdirname:
37 | cfgfile = "{tmpdir}/config.ini".format(tmpdir=tmpdirname)
38 | with open(cfgfile, "w+") as config:
39 | config.write(CONFIG)
40 | acp = AutoConfigParser(cfgfile)
41 | self.assertEqual(acp._configfile, cfgfile)
42 |
43 | def test_load(self):
44 | with tempfile.TemporaryDirectory() as tmpdirname:
45 | cfgfile = "{tmpdir}/config.ini".format(tmpdir=tmpdirname)
46 | with open(cfgfile, "w+") as config:
47 | config.write(CONFIG)
48 | acp = AutoConfigParser()
49 | acp.load(cfgfile)
50 |
51 | def test_save(self):
52 | with tempfile.TemporaryDirectory() as tmpdirname:
53 | cfgfile = "{tmpdir}/config.ini".format(tmpdir=tmpdirname)
54 | with open(cfgfile, "w+") as config:
55 | config.write(CONFIG)
56 | acp = AutoConfigParser(cfgfile)
57 | acp.load()
58 | acp.save()
59 | dump = "{tmpdir}/config2.ini".format(tmpdir=tmpdirname)
60 | acp.save(dump)
61 |
--------------------------------------------------------------------------------
/examples/http_adapter.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import argparse
3 | from pathlib import Path
4 | from typing import Optional
5 | import ssl
6 | from requests.adapters import HTTPAdapter
7 | from urllib3.poolmanager import PoolManager
8 |
9 | from redfish import redfish_client
10 |
11 |
12 | def make_dhe_compatible_context(
13 | cafile: Optional[Path] = None,
14 | *,
15 | seclevel: int = 1,
16 | verify: bool = True,
17 | tls12_only: bool = True,
18 | ) -> ssl.SSLContext:
19 | """
20 | Build an SSLContext that accepts legacy DHE handshakes (small DH groups).
21 | - seclevel=1 usually permits 1024-bit DH. Use 0 only as a last resort.
22 | - If verify=True and the server is self-signed, pass its PEM as `cafile`.
23 | - DHE is TLS<=1.2; set tls12_only=True to pin TLS 1.2.
24 | """
25 | ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
26 | ctx.set_ciphers(f"DEFAULT:@SECLEVEL={seclevel}:DHE")
27 | ctx.options |= ssl.OP_NO_COMPRESSION
28 | if tls12_only:
29 | ctx.minimum_version = ssl.TLSVersion.TLSv1_2
30 | ctx.maximum_version = ssl.TLSVersion.TLSv1_2
31 |
32 | if verify:
33 | if cafile:
34 | ctx.load_verify_locations(str(cafile))
35 | else:
36 | ctx.load_default_certs()
37 | else:
38 | ctx.check_hostname = False
39 | ctx.verify_mode = ssl.CERT_NONE
40 | return ctx
41 |
42 | class SSLContextAdapter(HTTPAdapter):
43 | """requests adapter that injects a custom ssl_context into urllib3."""
44 | def __init__(self, ssl_context: ssl.SSLContext, **kwargs):
45 | self._ssl_context = ssl_context
46 | super().__init__(**kwargs)
47 |
48 | def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
49 | pool_kwargs["ssl_context"] = self._ssl_context
50 | self.poolmanager = PoolManager(
51 | num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs
52 | )
53 |
54 | def proxy_manager_for(self, proxy, **proxy_kwargs):
55 | proxy_kwargs["ssl_context"] = self._ssl_context
56 | return super().proxy_manager_for(proxy, **proxy_kwargs)
57 |
58 |
59 | # Test dhe adapter
60 | ctx = make_dhe_compatible_context(seclevel=0, verify=False)
61 | adapter = SSLContextAdapter(ctx)
62 | parser = argparse.ArgumentParser( )
63 | parser.add_argument('url',help="Server with DHE encryption")
64 | args = parser.parse_args()
65 |
66 | client = redfish_client(args.url,https_adapter=adapter)
67 |
68 |
69 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Overview
4 |
5 | This repository is maintained by the [DMTF](https://www.dmtf.org/ "https://www.dmtf.org/"). All contributions are reviewed and approved by members of the organization.
6 |
7 | ## Submitting Issues
8 |
9 | Bugs, feature requests, and questions are all submitted in the "Issues" section for the project. DMTF members are responsible for triaging and addressing issues.
10 |
11 | ## Contribution Process
12 |
13 | 1. Fork the repository.
14 | 2. Make and commit changes.
15 | 3. Make a pull request.
16 |
17 | All contributions must adhere to the BSD 3-Clause License described in the LICENSE.md file, and the [Developer Certificate of Origin](#developer-certificate-of-origin).
18 |
19 | Pull requests are reviewed and approved by DMTF members.
20 |
21 | ## Developer Certificate of Origin
22 |
23 | All contributions must adhere to the [Developer Certificate of Origin (DCO)](http://developercertificate.org "http://developercertificate.org").
24 |
25 | The DCO is an attestation attached to every contribution made by every developer. In the commit message of the contribution, the developer adds a "Signed-off-by" statement and thereby agrees to the DCO. This can be added by using the `--signoff` parameter with `git commit`.
26 |
27 | Full text of the DCO:
28 |
29 | ```
30 | Developer Certificate of Origin
31 | Version 1.1
32 |
33 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
34 |
35 | Everyone is permitted to copy and distribute verbatim copies of this
36 | license document, but changing it is not allowed.
37 |
38 |
39 | Developer's Certificate of Origin 1.1
40 |
41 | By making a contribution to this project, I certify that:
42 |
43 | (a) The contribution was created in whole or in part by me and I
44 | have the right to submit it under the open source license
45 | indicated in the file; or
46 |
47 | (b) The contribution is based upon previous work that, to the best
48 | of my knowledge, is covered under an appropriate open source
49 | license and I have the right under that license to submit that
50 | work with modifications, whether created in whole or in part
51 | by me, under the same open source license (unless I am
52 | permitted to submit under a different license), as indicated
53 | in the file; or
54 |
55 | (c) The contribution was provided directly to me by some other
56 | person who certified (a), (b) or (c) and I have not modified
57 | it.
58 |
59 | (d) I understand and agree that this project and the contribution
60 | are public and that a record of the contribution (including all
61 | personal information I submit with it, including my sign-off) is
62 | maintained indefinitely and may be redistributed consistent with
63 | this project or the open source license(s) involved.
64 | ```
65 |
--------------------------------------------------------------------------------
/tests/rest/test_v1.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- encoding: utf-8 -*-
7 | import json
8 | import unittest
9 | from unittest import mock
10 |
11 | from redfish.rest.v1 import HttpClient, RetriesExhaustedError, redfish_client
12 |
13 |
14 | class TestRedFishClient(unittest.TestCase):
15 | def setUp(self) -> None:
16 | self.base_url = "http://foo.bar"
17 | self.username = "rstallman"
18 | self.password = "123456"
19 | self.default_prefix = "/custom/redfish/v1/"
20 | self.sessionkey = "fg687glgkf56vlgkf"
21 | self.capath = "/path/to/the/dir"
22 | self.cafile = "filename.test"
23 | self.timeout = 666
24 | self.max_retry = 42
25 |
26 | def test_redfish_client(self) -> None:
27 | # NOTE(hberaud) the client try to connect when we initialize the
28 | # http client object so we need to catch the retries exception first.
29 | # In a second time we need to mock the six.http_client to simulate
30 | # server responses and do some other tests
31 | with self.assertRaises(RetriesExhaustedError):
32 | client = redfish_client(base_url=self.base_url)
33 | # Check the object type
34 | self.assertTrue(isinstance(client, HttpClient))
35 | # Check the object attributes values.
36 | # Here we check if the client object is properly initialized
37 | self.assertEqual(client.base_url, self.base_url)
38 | self.assertEqual(client.username, self.username)
39 | self.assertEqual(client.password, self.password)
40 | self.assertEqual(client.default_prefix, self.default_prefix)
41 | self.assertEqual(client.sessionkey, self.sessionkey)
42 | self.assertEqual(client.capath, self.capath)
43 | self.assertEqual(client.cafile, self.cafile)
44 | self.assertEqual(client.timeout, self.timeout)
45 | self.assertEqual(client.max_retry, self.max_retry)
46 |
47 | def test_redfish_client_no_root_resp(self) -> None:
48 | client = redfish_client(base_url=self.base_url, check_connectivity=False)
49 | self.assertIsNone(getattr(client, "root_resp", None))
50 |
51 | @mock.patch("requests.Session.request")
52 | def test_redfish_client_root_object_initialized_after_login(
53 | self, mocked_request: mock.Mock
54 | ) -> None:
55 | dummy_root_data = '{"Links": {"Sessions": {"@data.id": "/redfish/v1/SessionService/Sessions"}}}'
56 | dummy_session_response = (
57 | '{"@odata.type": "#Session.v1_1_2.Session", '
58 | '"@odata.id": "/redfish/v1/SessionService/Sessions/1", '
59 | '"Id": "1", "Name": "User Session", "Description": "Manager User Session", '
60 | '"UserName": "user", "Oem": {}}'
61 | )
62 | root_resp = mock.Mock(content=dummy_root_data, status_code=200)
63 | auth_resp = mock.Mock(
64 | content=dummy_session_response,
65 | status_code=200,
66 | headers={"location": "fake", "x-auth-token": "fake"},
67 | )
68 | mocked_request.side_effect = [
69 | root_resp,
70 | auth_resp,
71 | ]
72 | client = redfish_client(base_url=self.base_url, check_connectivity=False)
73 | client.login()
74 |
75 | self.assertEqual(client.root, json.loads(dummy_root_data))
76 |
77 |
78 | if __name__ == "__main__":
79 | unittest.main()
80 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Release and Publish
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version:
6 | description: 'Version number'
7 | required: true
8 | changes_1:
9 | description: 'Change entry'
10 | required: true
11 | changes_2:
12 | description: 'Change entry'
13 | required: false
14 | changes_3:
15 | description: 'Change entry'
16 | required: false
17 | changes_4:
18 | description: 'Change entry'
19 | required: false
20 | changes_5:
21 | description: 'Change entry'
22 | required: false
23 | changes_6:
24 | description: 'Change entry'
25 | required: false
26 | changes_7:
27 | description: 'Change entry'
28 | required: false
29 | changes_8:
30 | description: 'Change entry'
31 | required: false
32 | jobs:
33 | release_build:
34 | name: Build the release
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/checkout@v2
38 | with:
39 | token: ${{secrets.GITHUB_TOKEN}}
40 | - name: Set up Python
41 | uses: actions/setup-python@v2
42 | with:
43 | python-version: '3.x'
44 | - name: Install dependencies
45 | run: |
46 | python -m pip install --upgrade pip
47 | pip install setuptools wheel twine
48 | - name: Build the changelog text
49 | run: |
50 | echo 'CHANGES<> $GITHUB_ENV
51 | echo "## [${{github.event.inputs.version}}] - $(date +'%Y-%m-%d')" >> $GITHUB_ENV
52 | echo "- ${{github.event.inputs.changes_1}}" >> $GITHUB_ENV
53 | if [[ -n "${{github.event.inputs.changes_2}}" ]]; then echo "- ${{github.event.inputs.changes_2}}" >> $GITHUB_ENV; fi
54 | if [[ -n "${{github.event.inputs.changes_3}}" ]]; then echo "- ${{github.event.inputs.changes_3}}" >> $GITHUB_ENV; fi
55 | if [[ -n "${{github.event.inputs.changes_4}}" ]]; then echo "- ${{github.event.inputs.changes_4}}" >> $GITHUB_ENV; fi
56 | if [[ -n "${{github.event.inputs.changes_5}}" ]]; then echo "- ${{github.event.inputs.changes_5}}" >> $GITHUB_ENV; fi
57 | if [[ -n "${{github.event.inputs.changes_6}}" ]]; then echo "- ${{github.event.inputs.changes_6}}" >> $GITHUB_ENV; fi
58 | if [[ -n "${{github.event.inputs.changes_7}}" ]]; then echo "- ${{github.event.inputs.changes_7}}" >> $GITHUB_ENV; fi
59 | if [[ -n "${{github.event.inputs.changes_8}}" ]]; then echo "- ${{github.event.inputs.changes_8}}" >> $GITHUB_ENV; fi
60 | echo "" >> $GITHUB_ENV
61 | echo 'EOF' >> $GITHUB_ENV
62 | - name: Update version numbers
63 | run: |
64 | sed -i -E 's/ version=.+,/ version='\'${{github.event.inputs.version}}\'',/' setup.py
65 | sed -i -E 's/__version__ = .+/__version__ = "'${{github.event.inputs.version}}'"/' src/redfish/__init__.py
66 | - name: Update the changelog
67 | run: |
68 | ex CHANGELOG.md <"
78 | git add CHANGELOG.md setup.py src/redfish/__init__.py
79 | git commit -s -m "${{github.event.inputs.version}} versioning"
80 | git push origin main
81 | - name: Make the release
82 | env:
83 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
84 | run: |
85 | gh release create ${{github.event.inputs.version}} -t ${{github.event.inputs.version}} -n "Changes since last release:"$'\n\n'"$CHANGES"
86 | - name: Build the distribution
87 | run: |
88 | python setup.py sdist bdist_wheel
89 | - name: Upload to pypi
90 | uses: pypa/gh-action-pypi-publish@release/v1
91 | with:
92 | password: ${{ secrets.PYPI_API_TOKEN }}
93 |
--------------------------------------------------------------------------------
/src/redfish/discovery/discovery.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """Discovers Redfish services"""
8 |
9 | import http.client
10 | import re
11 | import socket
12 |
13 | from io import BytesIO
14 |
15 |
16 | class FakeSocket:
17 | """Helper class to force raw data into an HTTP Response structure"""
18 |
19 | def __init__(self, response_str):
20 | self._file = BytesIO(response_str)
21 |
22 | def makefile(self, *args, **kwargs):
23 | return self._file
24 |
25 |
26 | def sanitize(number, minimum, maximum=None):
27 | """ Sanity check a given number.
28 |
29 | :param number: the number to check
30 | :param minimum: the minimum acceptable number
31 | :param maximum: the maximum acceptable number (optional)
32 |
33 | if maximum is not given sanitize return the given value superior
34 | at minimum
35 |
36 | :returns: an integer who respect the given allowed minimum and maximum
37 | """
38 | if number < minimum:
39 | number = minimum
40 | elif maximum is not None and number > maximum:
41 | number = maximum
42 | return number
43 |
44 |
45 | def discover_ssdp(port=1900, ttl=2, response_time=3, iface=None, protocol="ipv4", address=None):
46 | """Discovers Redfish services via SSDP
47 |
48 | :param port: the port to use for the SSDP request
49 | :type port: int
50 | :param ttl: the time-to-live value for the request
51 | :type ttl: int
52 | :param response_time: the number of seconds in which a service can respond
53 | :type response_time: int
54 | :param iface: the interface to use for the request; None for all
55 | :type iface: string
56 | :param protocol: the type of protocol to use for the request; either 'ipv4' or 'ipv6'
57 | :type protocol: string
58 | :param address: the address to use for the request; None for all
59 | :type address: string
60 |
61 | :returns: a set of discovery data
62 | """
63 | # Sanity check the inputs
64 | valid_protocols = ("ipv4", "ipv6")
65 | if protocol not in valid_protocols:
66 | raise ValueError("Invalid protocol type. Expected one of: {}".format(valid_protocols))
67 | ttl = sanitize(ttl, minimum=1, maximum=255)
68 | response_time = sanitize(response_time, minimum=1)
69 |
70 | if protocol == "ipv6":
71 | mcast_ip = "ff02::c"
72 | mcast_connection = (mcast_ip, port, 0, 0)
73 | af_type = socket.AF_INET6
74 | elif protocol == "ipv4":
75 | mcast_ip = "239.255.255.250"
76 | mcast_connection = (mcast_ip, port)
77 | af_type = socket.AF_INET
78 |
79 | # Initialize the multicast data
80 | msearch_str = (
81 | "M-SEARCH * HTTP/1.1\r\n"
82 | "Host: {}:{}\r\n"
83 | 'Man: "ssdp:discover"\r\n'
84 | "ST: urn:dmtf-org:service:redfish-rest:1\r\n"
85 | "MX: {}\r\n\r\n"
86 | ).format(mcast_ip, port, response_time)
87 | socket.setdefaulttimeout(response_time + 2)
88 |
89 | # Set up the socket and send the request
90 | sock = socket.socket(af_type, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
91 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
92 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
93 | if address:
94 | sock.bind((address, 0))
95 | if iface:
96 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, str(iface+"\0").encode("utf-8"))
97 | sock.sendto(bytearray(msearch_str, "utf-8"), mcast_connection)
98 |
99 | # On the same socket, wait for responses
100 | discovered_services = {}
101 | pattern = re.compile(
102 | r"^uuid:([a-f0-9\-]*)::urn:dmtf-org:service:redfish-rest:1(:\d+)?$") # noqa
103 | while True:
104 | try:
105 | response = http.client.HTTPResponse(FakeSocket(sock.recv(1024)))
106 | response.begin()
107 | uuid_search = pattern.search(response.getheader("USN").lower())
108 | if uuid_search:
109 | discovered_services[uuid_search.group(1)] = response.getheader(
110 | "AL"
111 | )
112 | except socket.timeout:
113 | # We hit the timeout; done waiting for responses
114 | break
115 |
116 | sock.close()
117 | return discovered_services
118 |
--------------------------------------------------------------------------------
/src/redfish/ris/config.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """Module for working with global configuration options."""
8 |
9 | #---------Imports---------
10 |
11 | import os
12 | import re
13 | import logging
14 | import configparser
15 |
16 | #---------End of imports---------
17 |
18 |
19 | #---------Debug logger---------
20 |
21 | LOGGER = logging.getLogger(__name__)
22 |
23 | #---------End of debug logger---------
24 |
25 | class AutoConfigParser(object):
26 | """Auto configuration parser"""
27 | # properties starting with _ac__ are automatically
28 | # serialized to config file
29 | _config_pattern = re.compile(r'_ac__(?P.*)')
30 |
31 | def __init__(self, filename=None):
32 | """Initialize AutoConfigParser
33 |
34 | :param filename: file name to be used for config loading.
35 | :type filename: str.
36 |
37 | """
38 | self._sectionname = 'globals'
39 | self._configfile = filename
40 |
41 | def _get_ac_keys(self):
42 | """Retrieve parse option keys"""
43 | result = []
44 | for key in self.__dict__:
45 | match = AutoConfigParser._config_pattern.search(key)
46 | if match:
47 | result.append(match.group('confkey'))
48 | return result
49 |
50 | def _get(self, key):
51 | """Retrieve parse option key
52 |
53 | :param key: key to retrieve.
54 | :type key: str.
55 |
56 | """
57 | ackey = '_ac__%s' % key.replace('-', '_')
58 | if ackey in self.__dict__:
59 | return self.__dict__[ackey]
60 | return None
61 |
62 | def _set(self, key, value):
63 | """Set parse option key
64 |
65 | :param key: key to be set.
66 | :type key: str.
67 | :param value: value to be given to key.
68 | :type value: str.
69 |
70 | """
71 | ackey = '_ac__%s' % key.replace('-', '_')
72 | if ackey in self.__dict__:
73 | self.__dict__[ackey] = value
74 | return None
75 |
76 | def load(self, filename=None):
77 | """Load configuration settings from the file filename, if filename"""
78 | """ is None then the value from get_configfile() is used
79 |
80 | :param filename: file name to be used for config loading.
81 | :type filename: str.
82 |
83 | """
84 | fname = self.get_configfile()
85 | if filename is not None and len(filename) > 0:
86 | fname = filename
87 |
88 | if fname is None or not os.path.isfile(fname):
89 | return
90 |
91 | try:
92 | config = configparser.RawConfigParser()
93 | config.read(fname)
94 | for key in self._get_ac_keys():
95 | configval = None
96 | try:
97 | configval = config.get(self._sectionname, key)
98 | except configparser.NoOptionError:
99 | # also try with - instead of _
100 | try:
101 | configval = config.get(self._sectionname,
102 | key.replace('_', '-'))
103 | except configparser.NoOptionError:
104 | pass
105 |
106 | if configval is not None and len(configval) > 0:
107 | ackey = '_ac__%s' % key
108 | self.__dict__[ackey] = configval
109 | except configparser.NoOptionError:
110 | pass
111 | except configparser.NoSectionError:
112 | pass
113 |
114 | def save(self, filename=None):
115 | """Save configuration settings from the file filename, if filename"""
116 | """ is None then the value from get_configfile() is used
117 |
118 | :param filename: file name to be used for config saving.
119 | :type filename: str.
120 |
121 | """
122 | fname = self.get_configfile()
123 | if filename is not None and len(filename) > 0:
124 | fname = filename
125 |
126 | if fname is None or len(fname) == 0:
127 | return
128 |
129 | config = configparser.RawConfigParser()
130 | try:
131 | config.add_section(self._sectionname)
132 | except configparser.DuplicateSectionError:
133 | pass # ignored
134 |
135 | for key in self._get_ac_keys():
136 | ackey = '_ac__%s' % key
137 | config.set(self._sectionname, key, str(self.__dict__[ackey]))
138 |
139 | fileh = open(self._configfile, 'w')
140 | config.write(fileh)
141 | fileh.close()
142 |
143 | def get_configfile(self):
144 | """ The current configuration file location"""
145 | return self._configfile
146 |
147 |
--------------------------------------------------------------------------------
/src/redfish/messages/messages.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python
2 | # Copyright Notice:
3 | # Copyright 2019-2020 DMTF. All rights reserved.
4 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md
5 |
6 | """
7 | Messages Module
8 |
9 | File : messages.py
10 |
11 | Brief : This file contains the definitions and functionalities for interacting
12 | with Messages for a given Redfish service
13 | """
14 | import re
15 |
16 | class RedfishOperationFailedError( Exception ):
17 | """
18 | Raised when an operation has failed (HTTP Status >= 400)
19 | """
20 | pass
21 |
22 | class RedfishPasswordChangeRequiredError( Exception ):
23 | """
24 | Raised when password change required
25 | """
26 | def __str__(self):
27 | return "\n{}\nURL: {}\n".format( str(self.args[0]), str(self.args[1]) )
28 |
29 | def get_messages_detail( response ):
30 | """
31 | Builds messages detail dict in the payload
32 |
33 | Args:
34 | response: The response to parser
35 |
36 | Returns:
37 | The dict containing messages_detail
38 | messages_detail["status"]: http status code
39 | messages_detail["successful"]: response successful (http status code < 400)
40 | messages_detail["code"]: redfish message response code field
41 | messages_detail["@Message.ExtendedInfo"]: redfish message response code field
42 | """
43 |
44 | messages_detail = {}
45 | messages_detail["status"] = response.status
46 | messages_detail["text"] = response.text
47 | messages_detail["successful"] = False
48 | messages_detail["@Message.ExtendedInfo"] = []
49 |
50 | if response.status >= 400:
51 | messages_detail["successful"] = False
52 | else:
53 | messages_detail["successful"] = True
54 |
55 | try:
56 | message_body = response.dict
57 | messages_detail["body"] = response.dict
58 |
59 | if not "@Message.ExtendedInfo" in message_body:
60 | message_body = response.dict["error"]
61 | check_message_field = True
62 | if "@Message.ExtendedInfo" in message_body:
63 | messages_detail["@Message.ExtendedInfo"] = message_body["@Message.ExtendedInfo"]
64 | for index in range(len(messages_detail["@Message.ExtendedInfo"])):
65 | messages_item = messages_detail["@Message.ExtendedInfo"][index]
66 | if not "MessageId" in messages_item:
67 | messages_item["MessageId"] = ""
68 | if not "Message" in messages_item:
69 | messages_item["Message"] = ""
70 | messages_detail["@Message.ExtendedInfo"][index] = messages_item
71 | check_message_field = False
72 |
73 | if check_message_field is True:
74 | messages_detail["@Message.ExtendedInfo"] = []
75 | messages_item = {}
76 | if "code" in message_body:
77 | messages_item["MessageId"] = message_body["code"]
78 | else:
79 | messages_item["MessageId"] = ""
80 | if "message" in message_body:
81 | messages_item["Message"] = message_body["message"]
82 | else:
83 | messages_item["Message"] = ""
84 | messages_detail["@Message.ExtendedInfo"].insert(0, messages_item)
85 | except:
86 | messages_detail["@Message.ExtendedInfo"] = []
87 | messages_detail["body"] = {}
88 |
89 | return messages_detail
90 |
91 | def search_message(response, message_registry_group, message_registry_id):
92 | """
93 | search message in the payload
94 |
95 | Args:
96 | response: The response to parser
97 | message_registry_group: target message_registry_group
98 | message_registry_id: target message_registry_id
99 | Returns:
100 | The dict containing target message detail
101 | """
102 | if isinstance(response, dict) and "@Message.ExtendedInfo" in response:
103 | messages_detail = response
104 | else:
105 | messages_detail = get_messages_detail(response)
106 |
107 | message_registry_id_search = "^" + message_registry_group + r"\.[0-9]+\.[0-9]+\." + message_registry_id +"$"
108 |
109 | for messages_item in messages_detail["@Message.ExtendedInfo"]:
110 | if "MessageId" in messages_item:
111 | resault = re.search(message_registry_id_search, messages_item["MessageId"])
112 | if resault:
113 | return messages_item
114 | return None
115 |
116 | def get_error_messages( response ):
117 | """
118 | Builds a string based on the error messages in the payload
119 |
120 | Args:
121 | response: The response to print
122 |
123 | Returns:
124 | The string containing error messages
125 | """
126 |
127 | # Pull out the error payload and the messages
128 |
129 | out_string = ""
130 | try:
131 | if isinstance(response, dict) and "@Message.ExtendedInfo" in response:
132 | messages_detail = response
133 | else:
134 | messages_detail = get_messages_detail(response)
135 |
136 | if "@Message.ExtendedInfo" in messages_detail:
137 | for message in messages_detail["@Message.ExtendedInfo"]:
138 | if "Message" in message:
139 | out_string = out_string + "\n" + message["MessageId"] + ": " + message["Message"]
140 | else:
141 | out_string = out_string + "\n" + message["MessageId"]
142 | out_string = out_string + "\n"
143 | except:
144 | # No response body
145 | out_string = ""
146 |
147 | return out_string
148 |
149 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [3.3.4] - 2025-08-29
4 | - Added option to allow for a user to specify an HTTPS adapter when building the client object
5 |
6 | ## [3.3.3] - 2025-07-25
7 | - Fixed formatting of error message string construction
8 |
9 | ## [3.3.2] - 2025-07-07
10 | - Fixed issue where headers were being propagated across different class instances
11 |
12 | ## [3.3.1] - 2025-03-28
13 | - Fixed bug in recent workaround logic for services not returning the 'Location' header to not print the workaround warning for failed login attempts
14 |
15 | ## [3.3.0] - 2025-03-21
16 | - Added workaround and warnings for session login when the service incorrectly does not provide the session location in the 'Location' response header
17 | - Minor typo fix in exception message for login failures
18 |
19 | ## [3.2.9] - 2025-03-07
20 | - Added optional 'headers' argument to the 'login' method
21 | - Clarified exception message when raising 'ServerDownOrUnreachableError'
22 |
23 | ## [3.2.8] - 2025-01-24
24 | - Updated 'dict' handling for responses without a body and 500 responses with a non-JSON body to use an empty dictionary
25 |
26 | ## [3.2.7] - 2024-12-24
27 | - Added JSON formatting of responses to debug logs
28 |
29 | ## [3.2.6] - 2024-11-15
30 | - Added workaround for services incorrectly responding with 401 when accessing the service root
31 |
32 | ## [3.2.5] - 2024-09-13
33 | - Added new 'check_connectivity' option when creating the client object
34 |
35 | ## [3.2.4] - 2024-08-09
36 | - No code changes; releasing again for PyPI publication
37 |
38 | ## [3.2.3] - 2024-08-09
39 | - Removed clearing of username and password to allow for sessions to be re-established
40 |
41 | ## [3.2.2] - 2024-01-19
42 | - Minor changes to fix Python 3.12 warnings with usage of raw strings
43 |
44 | ## [3.2.1] - 2023-08-04
45 | - Added 'timeout' and 'max_retry' parameters to all REST methods
46 | - Added exception to the method when a response contains a message indicating a password change is required
47 |
48 | ## [3.2.0] - 2023-07-27
49 | - Adding missing newline to M-SEARCH requests
50 | - Fixed the inspection of the USN response header from M-SEARCH requests to allow for a multi-digit minor version
51 |
52 | ## [3.1.9] - 2023-01-13
53 | - Improved usage of the ServerDownOrUnreachableError exception to not lose the original message
54 |
55 | ## [3.1.8] - 2022-12-02
56 | - Added request headers to debug log output
57 | - Added redacting of 'Password' properties from request bodies from debug logs
58 |
59 | ## [3.1.7] - 2022-09-09
60 | - Added handling for extracting error information when a session could not be created
61 |
62 | ## [3.1.6] - 2022-05-12
63 | - Fixed issue where the 'read' method on response objects always return strings
64 | - Modified query parameter encoding to not percent-encode characters allowed in query strings per RFC3986
65 |
66 | ## [3.1.5] - 2022-04-01
67 | - Added methods for specifying proxies directly with a new 'proxies' parameter
68 |
69 | ## [3.1.4] - 2022-03-25
70 | - Removed enforcement of trailing '/' in the 'default_prefix'
71 |
72 | ## [3.1.3] - 2022-03-21
73 | - Added support for Unix sockets
74 |
75 | ## [3.1.2] - 2022-03-10
76 | - Corrected usage of header storage and retrieval for static response objects
77 |
78 | ## [3.1.1] - 2022-01-18
79 | - Corrected 'import' statements to support Python 3.10
80 |
81 | ## [3.1.0] - 2022-01-10
82 | - Updated library to leverage 'requests' in favor of 'http.client'
83 |
84 | ## [3.0.3] - 2021-10-15
85 | - Added support for performing multi-part HTTP POST requests
86 |
87 | ## [3.0.2] - 2021-08-30
88 | - Added support for prepending 'https://' when the provided URI of the service does not contain a scheme
89 |
90 | ## [3.0.1] - 2021-06-04
91 | - Provided additional handling for HTTP 301 and 302 redirects
92 | - Changed session creation to not follow redirects in order to ensure the session token and location are not lost
93 | - Enhanced invalid JSON response handling to better highlight a service error
94 |
95 | ## [3.0.0] - 2021-02-20
96 | - Removed Python2 support
97 |
98 | ## [2.2.0] - 2021-02-15
99 | - Added support for `NO_PROXY` environment variable
100 |
101 | ## [2.1.9] - 2020-12-04
102 | - Added handling for HTTP 303 responses as part of redirect handling
103 |
104 | ## [2.1.8] - 2020-08-10
105 | - Added option to SSDP discover to bind to a specified address
106 | - Added ability to override built-in HTTP headers
107 | - Fixed issue where the location of a session was not being tracked properly for HTTP connections
108 |
109 | ## [2.1.7] - 2020-07-06
110 | - Added support for setting the 'Content-Type' header to 'application/octet-stream' when binary data is provided in a request
111 |
112 | ## [2.1.6] - 2020-06-12
113 | - Added support for leveraging the 'HTTP_PROXY' and 'HTTPS_PROXY' environment variables to set up proxy information
114 |
115 | ## [2.1.5] - 2020-02-03
116 | - Removed urlparse2 dependency
117 | - Updated jsonpatch requirements; jsonpatch 1.25 dropped Python 3.4 support
118 |
119 | ## [2.1.4] - 2020-01-10
120 | - Added fallback to using '/redfish/v1/SessionService/Sessions' if the service root does not contains the 'Links/Sessions' property for login
121 | - Added Python version checks to use time.perf_counter() in favor of time.clock()
122 |
123 | ## [2.1.3] - 2019-10-11
124 | - Added IPv6 support to SSDP discovery
125 | - Enhanced handling of poorly formatted URIs to not throw an exception
126 |
127 | ## [2.1.2] - 2019-09-16
128 | - Fixed usage of capath and cafile when setting them to None
129 |
130 | ## [2.1.1] - 2019-08-16
131 | - Added option in SSDP discovery to specify a particular interface
132 | - Added sanitization to the Base URL to remove trailing slashes
133 |
134 | ## [2.1.0] - 2019-07-12
135 | - Changed default authentication to be Session based
136 | - Removed unnecessary closing of sockets
137 |
138 | ## [2.0.9] - 2019-06-28
139 | - Added various unit tests and other cleanup
140 | - Added example for how to use the 'with' statement to perform automatically log out of a service
141 | - Made change to include the original trace when RetriesExhaustedError is encountered
142 |
143 | ## [2.0.8] - 2019-05-17
144 | - Added helper functions for Task processing
145 |
146 | ## [2.0.7] - 2019-02-08
147 | - Added optional timeout and max retry arguments
148 |
149 | ## [2.0.6] - 2019-01-11
150 | - Removed usage of setting the Content-Type header to application/x-www-form-urlencoded for PUT, POST, and PATCH methods
151 |
152 | ## [2.0.5] - 2018-11-30
153 | - Fixed handling of gzip content encoding
154 |
155 | ## [2.0.4] - 2018-10-26
156 | - Added discovery module with SSDP support
157 |
158 | ## [2.0.3] - 2018-10-19
159 | - Fixed handling of other successful HTTP responses (201, 202, and 204)
160 | - Added support for being able to check the certificate of a service
161 |
162 | ## [2.0.2] - 2018-09-07
163 | - Added handling for bad or dummy delete requests when logging out of a service
164 |
165 | ## [2.0.1] - 2018-05-25
166 | - Adjusting setup.py to contain correct information
167 |
168 | ## [2.0.0] - 2017-07-28
169 | - Python 3 Compatible Release
170 |
171 | ## [1.0.0] - 2017-01-12
172 | - Initial Public Release -- supports Redfish 1.0 features
173 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | python-redfish-library
2 | ======================
3 |
4 | .. image:: https://img.shields.io/pypi/v/redfish.svg?maxAge=2592000
5 | :target: https://pypi.python.org/pypi/redfish
6 | .. image:: https://img.shields.io/github/release/DMTF/python-redfish-library.svg?maxAge=2592000
7 | :target: https://github.com/DMTF/python-redfish-library/releases
8 | .. image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
9 | :target: https://raw.githubusercontent.com/DMTF/python-redfish-library/main/LICENSE
10 | .. image:: https://img.shields.io/pypi/pyversions/redfish.svg?maxAge=2592000
11 | :target: https://pypi.python.org/pypi/redfish
12 |
13 | .. contents:: :depth: 1
14 |
15 | Description
16 | -----------
17 |
18 | As of version 3.0.0, Python2 is no longer supported. If Python2 is required, ``redfish<3.0.0`` can be specified in a requirements file.
19 |
20 | REST (Representational State Transfer) is a web based software architectural style consisting of a set of constraints that focuses on a system's resources. The Redfish library performs GET, POST, PUT, PATCH and DELETE HTTP operations on resources within a Redfish service. Go to the `wiki <../../wiki>`_ for more details.
21 |
22 | Installing
23 | ----------
24 |
25 | .. code-block:: console
26 |
27 | pip install redfish
28 |
29 | Building from zip file source
30 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31 |
32 | .. code-block:: console
33 |
34 | python setup.py sdist --formats=zip (this will produce a .zip file)
35 | cd dist
36 | pip install redfish-x.x.x.zip
37 |
38 | Requirements
39 | ------------
40 |
41 | Ensure the system does not have the OpenStack "python-redfish" module installed on the target system. This module is using a conflicting package name that this library already uses. The module in question can be found here: https://pypi.org/project/python-redfish/
42 |
43 | Required external packages:
44 |
45 | .. code-block:: console
46 |
47 | jsonpatch<=1.24 ; python_version == '3.4'
48 | jsonpatch ; python_version >= '3.5'
49 | jsonpath_rw
50 | jsonpointer
51 | requests
52 | requests-toolbelt
53 | requests-unixsocket
54 |
55 | If installing from GitHub, you may install the external packages by running:
56 |
57 | .. code-block:: console
58 |
59 | pip install -r requirements.txt
60 |
61 | Usage
62 | ----------
63 |
64 | A set of examples is provided under the examples directory of this project. In addition to the directives present in this paragraph, you will find valuable implementation tips and tricks in those examples.
65 |
66 | Import the relevant Python module
67 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
68 |
69 | For a Redfish conformant application import the relevant Python module.
70 |
71 | For Redfish conformant application:
72 |
73 | .. code-block:: python
74 |
75 | import redfish
76 |
77 | Create a Redfish object
78 | ~~~~~~~~~~~~~~~~~~~~~~~
79 |
80 | The Redfish object contains three required parameters:
81 |
82 | * ``base_url``: The address of the Redfish service (with scheme). Example: ``https://192.168.1.100``. For Unix sockets, use the scheme ``http+unix://``, followed by the percent-encoded filepath to the socket.
83 | * ``username``: The username for authentication.
84 | * ``password``: The password for authentication.
85 |
86 | There are several optional parameters:
87 |
88 | * ``default_prefix``: The path to the Redfish service root. This is only used for initial connection and authentication with the service. The default value is ``/redfish/v1/``.
89 | * ``sessionkey``: The session key to use with subsequent requests. This can be used to bypass the login step. The default value is ``None``.
90 | * ``cafile``: The file path to the CA certificate that issued the Redfish service's certificate. The default value is ``None``.
91 | * ``timeout``: The number of seconds to wait for a response before closing the connection. The default value is ``None``.
92 | * ``max_retry``: The number of retries to perform an operation before giving up. The default value is ``10``.
93 | * ``proxies``: A dictionary containing protocol to proxy URL mappings. The default value is ``None``. See `Using proxies`_.
94 | * ``check_connectivity``: A boolean value to determine whether the client immediately attempts a connection to the base_url. The default is ``True``.
95 |
96 | To create a Redfish object, call the ``redfish_client`` method:
97 |
98 | .. code-block:: python
99 |
100 | REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account, \
101 | password=login_password, default_prefix='/redfish/v1/')
102 |
103 | Login to the service
104 | ~~~~~~~~~~~~~~~~~~~~
105 |
106 | After creating the REDFISH_OBJ, perform the ``login`` operation to authenticate with the service. The ``auth`` parameter allows you to specify the login method. Possible values are:
107 |
108 | * ``session``: Creates a Redfish session with a session token.
109 | * ``basic``: Uses HTTP Basic authentication for all requests.
110 |
111 | .. code-block:: python
112 |
113 | REDFISH_OBJ.login(auth="session")
114 |
115 | Perform a GET operation
116 | ~~~~~~~~~~~~~~~~~~~~~~~
117 |
118 | A simple GET operation can be performed to obtain the data present in any valid path.
119 | An example of GET operation on the path "/redfish/v1/Systems/1" is shown below:
120 |
121 | .. code-block:: python
122 |
123 | response = REDFISH_OBJ.get("/redfish/v1/Systems/1")
124 |
125 | Perform a POST operation
126 | ~~~~~~~~~~~~~~~~~~~~~~~~
127 |
128 | A POST operation can be performed to create a resource or perform an action.
129 | An example of a POST operation on the path "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset" is shown below:
130 |
131 | .. code-block:: python
132 |
133 | body = {"ResetType": "GracefulShutdown"}
134 | response = REDFISH_OBJ.post("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", body=body)
135 |
136 | Notes about HTTP methods and arguments
137 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
138 |
139 | The previous sections showed example GET and POST requests. The following is a list of the different methods supported:
140 |
141 | * ``get``: Performs an HTTP GET operation to retrieve a resource from a URI.
142 | * ``head``: Performs an HTTP HEAD operation to retrieve response headers from a URI, but no body.
143 | * ``post``: Performs an HTTP POST operation to perform an action or create a new resource.
144 | * ``put``: Performs an HTTP PUT operation to replace an existing resource.
145 | * ``patch``: Performs an HTTP PATCH operation to update an existing resource.
146 | * ``delete``: Performs an HTTP DELETE operation to remove a resource.
147 |
148 | Each of the previous methods allows for the following arguments:
149 |
150 | * ``path``: **Required**. String. The URI in which to invoke the operation.
151 |
152 | - Example: ``"/redfish/v1/Systems/1"``
153 |
154 | * ``args``: Dictionary. Query parameters to supply with the request.
155 |
156 | - The key-value pairs in the dictionary are the query parameter name and the query parameter value to supply.
157 | - Example: ``{"$select": "Reading,Status"}``
158 |
159 | * ``body``: Dictionary, List, Bytes, or String. The request body to provide with the request.
160 |
161 | - Not supported for ``get``, ``head``, or ``delete`` methods.
162 | - The data type supplied will dictate the encoding.
163 | - A dictionary is the most common usage, which results in a JSON body.
164 | - Example: ``{"ResetType": "GracefulShutdown"}``
165 | - A list is used to supply multipart forms, which is useful for multipart HTTP push updates.
166 | - Bytes is used to supply an octet stream.
167 | - A string is used to supply an unstructed body, which may be used in some OEM cases.
168 |
169 | * ``headers``: Dictionary. Additional HTTP headers to supply with the request.
170 |
171 | - The key-value pairs in the dictionary are the HTTP header name and the HTTP header value to supply.
172 | - Example: ``{"If-Match": etag_value}``
173 |
174 | * ``timeout``: Number. The number of seconds to wait for a response before closing the connection for this request.
175 |
176 | - Overrides the timeout value specified when the Redfish object is created for this request.
177 | - This can be useful when a particular URI is known to take a long time to respond, such as with firmware updates.
178 | - The default value is ``None``, which indicates the object-defined timeout is used.
179 |
180 | * ``max_retry``: Number. The number of retries to perform an operation before giving up for this request.
181 |
182 | - Overrides the max retry value specified when the Redfish object is created for this request.
183 | - This can be useful when a particular URI is known to take multiple retries.
184 | - The default value is ``None``, which indicates the object-defined max retry count is used.
185 |
186 | Working with tasks
187 | ~~~~~~~~~~~~~~~~~~
188 |
189 | POST, PATCH, PUT, and DELETE operations may result in a task, describing an operation with a duration greater than the span of a single request.
190 | The action message object that ``is_processing`` will return a task that can be accessed reviewed when polled with monitor.
191 | An example of a POST operation with a possible task is shown below.
192 |
193 | .. code-block:: python
194 |
195 | body = {"ResetType": "GracefulShutdown"}
196 | response = REDFISH_OBJ.post("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", body=body)
197 | if(response.is_processing):
198 | task = response.monitor(REDFISH_OBJ)
199 |
200 | while(task.is_processing):
201 | retry_time = task.retry_after
202 | task_status = task.dict['TaskState']
203 | time.sleep(retry_time if retry_time else 5)
204 | task = response.monitor(REDFISH_OBJ)
205 |
206 | Logout the created session
207 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
208 |
209 | Ensure you perform a ``logout`` operation when done interacting with the Redfish service. If this step isn't performed, the session will remain active until the Redfish service decides to close it.
210 |
211 | .. code-block:: python
212 |
213 | REDFISH_OBJ.logout()
214 |
215 | The ``logout`` operation deletes the current sesssion from the service. The ``redfish_client`` object destructor includes a logout statement.
216 |
217 | Using proxies
218 | ~~~~~~~~~~~~~
219 |
220 | There are two methods for using proxies: configuring environment variables or directly providing proxy information.
221 |
222 | Environment variables
223 | ^^^^^^^^^^^^^^^^^^^^^
224 |
225 | You can use a proxy by specifying the ``HTTP_PROXY`` and ``HTTPS_PROXY`` environment variables. Hosts to be excluded from the proxy can be specified using the NO_PROXY environment variable.
226 |
227 | .. code-block:: shell
228 |
229 | export HTTP_PROXY="http://192.168.1.10:8888"
230 | export HTTPS_PROXY="http://192.168.1.10:8888"
231 |
232 | Directly provided
233 | ^^^^^^^^^^^^^^^^^
234 |
235 | You can use a proxy by building a dictionary containing the proxy information and providing it to the ``proxies`` argument when creating the ``redfish_client`` object.
236 | The key-value pairs of the dictionary contain the protocol and the proxy URL for the protocol.
237 |
238 | .. code-block:: python
239 |
240 | proxies = {
241 | 'http': 'http://192.168.1.10:8888',
242 | 'https': 'http://192.168.1.10:8888',
243 | }
244 | REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account, \
245 | password=login_password, proxies=proxies)
246 |
247 | SOCKS proxy support
248 | ^^^^^^^^^^^^^^^^^^^
249 |
250 | An additional package is required to use SOCKS proxies.
251 |
252 | .. code-block:: console
253 |
254 | pip install -U requests[socks]
255 |
256 | Once installed, the proxy can be configured using environment variables or directly provided like any other proxy.
257 | For example:
258 |
259 | .. code-block:: shell
260 |
261 | export HTTP_PROXY="socks5h://localhost:8123"
262 | export HTTPS_PROXY="socks5h://localhost:8123"
263 |
264 | Release Process
265 | ---------------
266 |
267 | 1. Go to the "Actions" page
268 | 2. Select the "Release and Publish" workflow
269 | 3. Click "Run workflow"
270 | 4. Fill out the form
271 | 5. Click "Run workflow"
272 |
273 | Copyright and License
274 | ---------------------
275 |
276 | Copyright Notice:
277 | Copyright 2016-2022 DMTF. All rights reserved.
278 | License: BSD 3-Clause License. For full text see link: `https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md `_
279 |
--------------------------------------------------------------------------------
/src/redfish/ris/ris.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """RIS implementation"""
8 |
9 | # ---------Imports---------
10 |
11 | import abc
12 | import logging
13 | import sys
14 | import threading
15 | from queue import Queue
16 | from collections import OrderedDict
17 |
18 | import jsonpath_rw
19 |
20 | import redfish.rest.v1
21 | from redfish.ris.sharedtypes import Dictable
22 |
23 | from urllib.parse import urlparse, urlunparse
24 |
25 |
26 | # ---------End of imports---------
27 |
28 | # ---------Debug logger---------
29 |
30 | LOGGER = logging.getLogger(__name__)
31 |
32 | # ---------End of debug logger---------
33 |
34 |
35 | class SessionExpiredRis(Exception):
36 | """Raised when session has expired"""
37 |
38 | pass
39 |
40 |
41 | class RisMonolithMemberBase(Dictable):
42 | """RIS monolith member base class"""
43 | __metaclass__ = abc.ABCMeta
44 |
45 | pass
46 |
47 |
48 | class RisMonolithMember_v1_0_0(RisMonolithMemberBase):
49 | """Wrapper around RestResponse that adds the monolith data"""
50 |
51 | def __init__(self, restresp):
52 | self._resp = restresp
53 | self._patches = list()
54 | self._type = None
55 | self._typestring = "@odata.type"
56 |
57 | def _get_type(self):
58 | """Return type from monolith"""
59 | if self._typestring in self._resp.dict:
60 | return self._resp.dict[self._typestring]
61 | elif "type" in self._resp.dict:
62 | return self._resp.dict["type"]
63 | return None
64 |
65 | type = property(_get_type, None)
66 |
67 | def _get_maj_type(self):
68 | """Return maj type from monolith"""
69 | if self.type:
70 | return self.type[:-4]
71 | return None
72 |
73 | maj_type = property(_get_maj_type, None)
74 |
75 | def _get_resp(self):
76 | """Return resp from monolith"""
77 | return self._resp
78 |
79 | resp = property(_get_resp, None)
80 |
81 | def _get_patches(self):
82 | """Return patches from monolith"""
83 | return self._patches
84 |
85 | patches = property(_get_patches, None)
86 |
87 | def to_dict(self):
88 | """Convert monolith to dict"""
89 | result = OrderedDict()
90 | if self.type:
91 | result["Type"] = self.type
92 |
93 | if (self.maj_type == "Collection.1"
94 | and "MemberType" in self._resp.dict):
95 | result["MemberType"] = self._resp.dict["MemberType"]
96 |
97 | result["links"] = OrderedDict()
98 | result["links"]["href"] = ""
99 | headers = dict()
100 |
101 | for header in self._resp.getheaders():
102 | headers[header[0]] = header[1]
103 |
104 | result["Headers"] = headers
105 |
106 | if "etag" in headers:
107 | result["ETag"] = headers["etag"]
108 |
109 | result["OriginalUri"] = self._resp.request.path
110 | result["Content"] = self._resp.dict
111 | result["Patches"] = self._patches
112 |
113 | return result
114 |
115 | def load_from_dict(self, src):
116 | """Load variables from dict monolith"""
117 | """
118 |
119 | :param src: source to load from
120 | :type src: dict
121 |
122 | """
123 | if "Type" in src:
124 | self._type = src["Type"]
125 | restreq = redfish.rest.v1.RestRequest(method="GET",
126 | path=src["OriginalUri"])
127 |
128 | src["restreq"] = restreq
129 | self._resp = redfish.rest.v1.StaticRestResponse(**src)
130 | self._patches = src["Patches"]
131 |
132 | def _reducer(self, indict, breadcrumbs=None, outdict=OrderedDict()):
133 | """Monolith reducer
134 |
135 | :param indict: input dictionary.
136 | :type indict: dict.
137 | :param breadcrumbs: breadcrumbs from previous operations.
138 | :type breadcrumbs: dict.
139 | :param outdict: expected output format.
140 | :type outdict: dictionary type.
141 | :returns: returns outdict
142 |
143 | """
144 | if breadcrumbs is None:
145 | breadcrumbs = []
146 |
147 | if isinstance(indict, dict):
148 | for key, val in list(indict.items()):
149 | breadcrumbs.append(key) # push
150 |
151 | if isinstance(val, dict):
152 | self._reducer(val, breadcrumbs, outdict)
153 | elif isinstance(val, list) or isinstance(val, tuple):
154 | for i in range(0, len(val)):
155 | breadcrumbs.append("%s" % i) # push
156 | self._reducer(val[i], breadcrumbs, outdict)
157 |
158 | del breadcrumbs[-1] # pop
159 | elif isinstance(val, tuple):
160 | self._reducer(val, breadcrumbs, outdict)
161 | else:
162 | self._reducer(val, breadcrumbs, outdict)
163 |
164 | del breadcrumbs[-1] # pop
165 | else:
166 | outkey = "/".join(breadcrumbs)
167 | outdict[outkey] = indict
168 |
169 | return outdict
170 |
171 | def _jsonpath_reducer(self,
172 | indict,
173 | breadcrumbs=None,
174 | outdict=OrderedDict()):
175 | """JSON Path Reducer
176 |
177 | :param indict: input dictionary.
178 | :type indict: dict.
179 | :param breadcrumbs: breadcrumbs from previous operations.
180 | :type breadcrumbs: dict.
181 | :param outdict: expected output format.
182 | :type outdict: dictionary type.
183 | :returns: returns outdict
184 |
185 | """
186 | if breadcrumbs is None:
187 | breadcrumbs = []
188 |
189 | if isinstance(indict, dict):
190 | for key, val in list(indict.items()):
191 | breadcrumbs.append(key) # push
192 |
193 | if isinstance(val, dict):
194 | self._reducer(val, breadcrumbs, outdict)
195 | elif isinstance(val, list) or isinstance(val, tuple):
196 | for i in range(0, len(val)):
197 | breadcrumbs.append("[%s]" % i) # push
198 | self._reducer(val[i], breadcrumbs, outdict)
199 |
200 | del breadcrumbs[-1] # pop
201 | elif isinstance(val, tuple):
202 | self._reducer(val, breadcrumbs, outdict)
203 | else:
204 | self._reducer(val, breadcrumbs, outdict)
205 |
206 | del breadcrumbs[-1] # pop
207 | else:
208 | outkey = ".".join(breadcrumbs)
209 | outkey = outkey.replace(".[", "[")
210 | outdict[outkey] = indict
211 |
212 | return outdict
213 |
214 | def reduce(self):
215 | """Returns a "flatten" dict with nested data represented in"""
216 | """ JSONpath notation"""
217 | result = OrderedDict()
218 |
219 | if self.type:
220 | result["Type"] = self.type
221 |
222 | if (self.maj_type == "Collection.1"
223 | and "MemberType" in self._resp.dict):
224 | result["MemberType"] = self._resp.dict["MemberType"]
225 |
226 | self._reducer(self._resp.dict)
227 | result["OriginalUri"] = self._resp.request.path
228 | result["Content"] = self._reducer(self._resp.dict)
229 |
230 | return result
231 |
232 |
233 | class RisMonolith_v1_0_0(Dictable):
234 | """Monolithic cache of RIS data"""
235 |
236 | def __init__(self, client):
237 | """Initialize RisMonolith
238 |
239 | :param client: client to utilize
240 | :type client: RmcClient object
241 |
242 | """
243 | self._client = client
244 | self.name = "Monolithic output of RIS Service"
245 | self.types = OrderedDict()
246 | self._visited_urls = list()
247 | self._current_location = "/" # "root"
248 | self.queue = Queue()
249 | self._type = None
250 | self._name = None
251 | self.progress = 0
252 | self.reload = False
253 |
254 | self._typestring = "@odata.type"
255 | self._hrefstring = "@odata.id"
256 |
257 | def _get_type(self):
258 | """Return monolith version type"""
259 | return "Monolith.1.0.0"
260 |
261 | type = property(_get_type, None)
262 |
263 | def update_progress(self):
264 | """Simple function to increment the dot progress"""
265 | if self.progress % 6 == 0:
266 | sys.stdout.write(".")
267 |
268 | def get_visited_urls(self):
269 | """Return the visited URLS"""
270 | return self._visited_urls
271 |
272 | def set_visited_urls(self, visited_urls):
273 | """Set visited URLS to given list."""
274 | self._visited_urls = visited_urls
275 |
276 | def load(
277 | self,
278 | path=None,
279 | includelogs=False,
280 | skipinit=False,
281 | skipcrawl=False,
282 | loadtype="href",
283 | loadcomplete=False,
284 | ):
285 | """Walk entire RIS model and cache all responses in self.
286 |
287 | :param path: path to start load from.
288 | :type path: str.
289 | :param includelogs: flag to determine if logs should be downloaded also.
290 | :type includelogs: boolean.
291 | :param skipinit: flag to determine if first run of load.
292 | :type skipinit: boolean.
293 | :param skipcrawl: flag to determine if load should traverse found links.
294 | :type skipcrawl: boolean.
295 | :param loadtype: flag to determine if load is meant for only href items.
296 | :type loadtype: str.
297 | :param loadcomplete: flag to download the entire monolith
298 | :type loadcomplete: boolean
299 |
300 | """ # noqa
301 | if not skipinit:
302 | if LOGGER.getEffectiveLevel() == 40:
303 | sys.stdout.write("Discovering data...")
304 | else:
305 | LOGGER.info("Discovering data...")
306 | self.name = self.name + " at %s" % self._client.base_url
307 |
308 | if not self.types:
309 | self.types = OrderedDict()
310 |
311 | if not threading.active_count() >= 6:
312 | for _ in range(5):
313 | workhand = SuperDuperWorker(self.queue)
314 | workhand.setDaemon(True)
315 | workhand.start()
316 |
317 | selectivepath = path
318 | if not selectivepath:
319 | selectivepath = self._client._rest_client.default_prefix
320 |
321 | self._load(
322 | selectivepath,
323 | skipcrawl=skipcrawl,
324 | includelogs=includelogs,
325 | skipinit=skipinit,
326 | loadtype=loadtype,
327 | loadcomplete=loadcomplete,
328 | )
329 | self.queue.join()
330 |
331 | if not skipinit:
332 | if LOGGER.getEffectiveLevel() == 40:
333 | sys.stdout.write("Done\n")
334 | else:
335 | LOGGER.info("Done\n")
336 |
337 | def _load(
338 | self,
339 | path,
340 | skipcrawl=False,
341 | originaluri=None,
342 | includelogs=False,
343 | skipinit=False,
344 | loadtype="href",
345 | loadcomplete=False,
346 | ):
347 | """Helper function to main load function.
348 |
349 | :param path: path to start load from.
350 | :type path: str.
351 | :param skipcrawl: flag to determine if load should traverse found links.
352 | :type skipcrawl: boolean.
353 | :param originaluri: variable to assist in determining originating path.
354 | :type originaluri: str.
355 | :param includelogs: flag to determine if logs should be downloaded also.
356 | :type includelogs: boolean.
357 | :param skipinit: flag to determine if first run of load.
358 | :type skipinit: boolean.
359 | :param loadtype: flag to determine if load is meant for only href items.
360 | :type loadtype: str.
361 | :param loadcomplete: flag to download the entire monolith
362 | :type loadcomplete: boolean
363 |
364 | """ # noqa
365 | if path.endswith("?page=1"):
366 | return
367 | elif not includelogs:
368 | if "/Logs/" in path:
369 | return
370 |
371 | # catch any exceptions during URL parsing or GET requests
372 | try:
373 | # remove fragments
374 | newpath = urlparse(path)
375 | newpath = list(newpath[:])
376 | newpath[-1] = ""
377 |
378 | path = urlunparse(tuple(newpath))
379 |
380 | LOGGER.debug("_loading %s", path)
381 |
382 | if not self.reload:
383 | if path.lower() in self._visited_urls:
384 | return
385 |
386 | resp = self._client.get(path)
387 |
388 | if resp.status != 200:
389 | path = path + "/"
390 | resp = self._client.get(path)
391 |
392 | if resp.status == 401:
393 | raise SessionExpiredRis("Invalid session. Please logout and "
394 | "log back in or include credentials.")
395 | elif resp.status != 200:
396 | return
397 | except SessionExpiredRis:
398 | raise
399 | except Exception as e:
400 | cause = e.__cause__ if e.__cause__ else e
401 | LOGGER.error("Resource '{}' skipped due to exception: {}"
402 | .format(path, repr(cause)))
403 | return
404 |
405 | self.queue.put((resp, path, skipinit, self))
406 |
407 | if loadtype == "href":
408 | # follow all the href attributes
409 | jsonpath_expr = jsonpath_rw.parse("$..'@odata.id'")
410 | matches = jsonpath_expr.find(resp.dict)
411 |
412 | if "links" in resp.dict and "NextPage" in resp.dict["links"]:
413 | if originaluri:
414 | next_link_uri = (
415 | originaluri + "?page=" +
416 | str(resp.dict["links"]["NextPage"]["page"]))
417 | href = "%s" % next_link_uri
418 |
419 | self._load(
420 | href,
421 | originaluri=originaluri,
422 | includelogs=includelogs,
423 | skipcrawl=skipcrawl,
424 | skipinit=skipinit,
425 | )
426 | else:
427 | next_link_uri = (
428 | path + "?page=" +
429 | str(resp.dict["links"]["NextPage"]["page"]))
430 |
431 | href = "%s" % next_link_uri
432 | self._load(
433 | href,
434 | originaluri=path,
435 | includelogs=includelogs,
436 | skipcrawl=skipcrawl,
437 | skipinit=skipinit,
438 | )
439 |
440 | if not skipcrawl:
441 | for match in matches:
442 | if (str(match.full_path) == "Registries.@odata.id" or str(
443 | match.full_path) == "JsonSchemas.@odata.id"):
444 | continue
445 |
446 | if match.value == path:
447 | continue
448 |
449 | href = "%s" % match.value
450 | self._load(
451 | href,
452 | skipcrawl=skipcrawl,
453 | originaluri=originaluri,
454 | includelogs=includelogs,
455 | skipinit=skipinit,
456 | )
457 |
458 | if loadcomplete:
459 | for match in matches:
460 | self._load(
461 | match.value,
462 | skipcrawl=skipcrawl,
463 | originaluri=originaluri,
464 | includelogs=includelogs,
465 | skipinit=skipinit,
466 | )
467 |
468 | def branch_worker(self, resp, path, skipinit):
469 | """Helper for load function, creates threaded worker
470 |
471 | :param resp: response received.
472 | :type resp: str.
473 | :param path: path correlating to the response.
474 | :type path: str.
475 | :param skipinit: flag to determine if progress bar should be updated.
476 | :type skipinit: boolean.
477 |
478 | """
479 | self._visited_urls.append(path.lower())
480 |
481 | member = RisMonolithMember_v1_0_0(resp)
482 | if not member.type:
483 | return
484 |
485 | self.update_member(member)
486 |
487 | if not skipinit:
488 | self.progress += 1
489 | if LOGGER.getEffectiveLevel() == 40:
490 | self.update_progress()
491 |
492 | def update_member(self, member):
493 | """Adds member to this monolith. If the member already exists the"""
494 | """ data is updated in place.
495 |
496 | :param member: Ris monolith member object made by branch worker.
497 | :type member: RisMonolithMember_v1_0_0.
498 |
499 | """
500 | if member.maj_type not in self.types:
501 | self.types[member.maj_type] = OrderedDict()
502 | self.types[member.maj_type]["Instances"] = list()
503 |
504 | found = False
505 |
506 | for indices in range(len(self.types[member.maj_type]["Instances"])):
507 | inst = self.types[member.maj_type]["Instances"][indices]
508 |
509 | if inst.resp.request.path == member.resp.request.path:
510 | self.types[member.maj_type]["Instances"][indices] = member
511 | self.types[
512 | member.maj_type]["Instances"][indices].patches.extend(
513 | [patch for patch in inst.patches])
514 |
515 | found = True
516 | break
517 |
518 | if not found:
519 | self.types[member.maj_type]["Instances"].append(member)
520 |
521 | def load_from_dict(self, src):
522 | """Load data to monolith from dict
523 |
524 | :param src: data receive from rest operation.
525 | :type src: str.
526 |
527 | """
528 | self._type = src["Type"]
529 | self._name = src["Name"]
530 | self.types = OrderedDict()
531 |
532 | for typ in src["Types"]:
533 | for inst in typ["Instances"]:
534 | member = RisMonolithMember_v1_0_0(None)
535 | member.load_from_dict(inst)
536 | self.update_member(member)
537 |
538 | return
539 |
540 | def to_dict(self):
541 | """Convert data to monolith from dict"""
542 | result = OrderedDict()
543 | result["Type"] = self.type
544 | result["Name"] = self.name
545 | types_list = list()
546 |
547 | for typ in list(self.types.keys()):
548 | type_entry = OrderedDict()
549 | type_entry["Type"] = typ
550 | type_entry["Instances"] = list()
551 |
552 | for inst in self.types[typ]["Instances"]:
553 | type_entry["Instances"].append(inst.to_dict())
554 |
555 | types_list.append(type_entry)
556 |
557 | result["Types"] = types_list
558 | return result
559 |
560 | def reduce(self):
561 | """Reduce monolith data"""
562 | result = OrderedDict()
563 | result["Type"] = self.type
564 | result["Name"] = self.name
565 | types_list = list()
566 |
567 | for typ in list(self.types.keys()):
568 | type_entry = OrderedDict()
569 | type_entry["Type"] = typ
570 |
571 | for inst in self.types[typ]["Instances"]:
572 | type_entry["Instances"] = inst.reduce()
573 |
574 | types_list.append(type_entry)
575 |
576 | result["Types"] = types_list
577 | return result
578 |
579 | def _jsonpath2jsonpointer(self, instr):
580 | """Convert json path to json pointer
581 |
582 | :param instr: input path to be converted to pointer.
583 | :type instr: str.
584 |
585 | """
586 | outstr = instr.replace(".[", "[")
587 | outstr = outstr.replace("[", "/")
588 | outstr = outstr.replace("]", "/")
589 |
590 | if outstr.endswith("/"):
591 | outstr = outstr[:-1]
592 |
593 | return outstr
594 |
595 | def _get_current_location(self):
596 | """Return current location"""
597 | return self._current_location
598 |
599 | def _set_current_location(self, newval):
600 | """Set current location"""
601 | self._current_location = newval
602 |
603 | location = property(_get_current_location, _set_current_location)
604 |
605 | def list(self, lspath=None):
606 | """Function for list command
607 |
608 | :param lspath: path list.
609 | :type lspath: list.
610 |
611 | """
612 | results = list()
613 | path_parts = ["Types"] # Types is always assumed
614 |
615 | if isinstance(lspath, list) and len(lspath) > 0:
616 | lspath = lspath[0]
617 | path_parts.extend(lspath.split("/"))
618 | elif not lspath:
619 | lspath = "/"
620 | else:
621 | path_parts.extend(lspath.split("/"))
622 |
623 | currpos = self.to_dict()
624 | for path_part in path_parts:
625 | if not path_part:
626 | continue
627 |
628 | if isinstance(currpos, RisMonolithMember_v1_0_0):
629 | break
630 | elif isinstance(currpos, dict) and path_part in currpos:
631 | currpos = currpos[path_part]
632 | elif isinstance(currpos, list):
633 | for positem in currpos:
634 | if "Type" in positem and path_part == positem["Type"]:
635 | currpos = positem
636 | break
637 |
638 | results.append(currpos)
639 |
640 | return results
641 |
642 | def killthreads(self):
643 | """Function to kill threads on logout"""
644 | threads = []
645 | for thread in threading.enumerate():
646 | if isinstance(thread, SuperDuperWorker):
647 | self.queue.put(("KILL", "KILL", "KILL", "KILL"))
648 | threads.append(thread)
649 |
650 | for thread in threads:
651 | thread.join()
652 |
653 |
654 | class RisMonolith(RisMonolith_v1_0_0):
655 | """Latest implementation of RisMonolith"""
656 |
657 | def __init__(self, client):
658 | """Initialize Latest RisMonolith
659 |
660 | :param client: client to utilize
661 | :type client: RmcClient object
662 |
663 | """
664 | super(RisMonolith, self).__init__(client)
665 |
666 |
667 | class SuperDuperWorker(threading.Thread):
668 | """Recursive worker implementation"""
669 |
670 | def __init__(self, queue):
671 | """Initialize SuperDuperWorker
672 |
673 | :param queue: queue for worker
674 | :type queue: Queue object
675 |
676 | """
677 | threading.Thread.__init__(self)
678 | self.queue = queue
679 |
680 | def run(self):
681 | """Thread creator"""
682 | while True:
683 | (resp, path, skipinit, thobj) = self.queue.get()
684 | if (resp == "KILL" and path == "KILL" and skipinit == "KILL"
685 | and thobj == "KILL"):
686 | break
687 | thobj.branch_worker(resp, path, skipinit)
688 | self.queue.task_done()
689 |
--------------------------------------------------------------------------------
/src/redfish/ris/rmc_helper.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """RMC helper implementation"""
8 |
9 | #---------Imports---------
10 |
11 | import os
12 | import json
13 | import errno
14 | import logging
15 | import hashlib
16 | import redfish.rest
17 |
18 | from urllib.parse import urlparse
19 |
20 | from .ris import (RisMonolith)
21 | from .sharedtypes import (JSONEncoder)
22 | from .config import (AutoConfigParser)
23 |
24 | #---------End of imports---------
25 |
26 | #---------Debug logger---------
27 |
28 | LOGGER = logging.getLogger(__name__)
29 |
30 | #---------End of debug logger---------
31 |
32 | class RdmcError(Exception):
33 | """Base class for all RDMC Exceptions"""
34 | errcode = 1
35 | def __init__(self, message):
36 | Exception.__init__(self, message)
37 |
38 | class InvalidCommandLineError(RdmcError):
39 | """Raised when user enter incorrect command line arguments"""
40 | pass
41 |
42 | class FailureDuringCommitError(RdmcError):
43 | """Raised when there is an error while updating firmware"""
44 | pass
45 |
46 | class UndefinedClientError(Exception):
47 | """Raised when there are no clients active (usually when user hasn't logged in"""
48 | pass
49 |
50 | class InstanceNotFoundError(Exception):
51 | """Raised when attempting to select an instance that does not exist"""
52 | pass
53 |
54 | class CurrentlyLoggedInError(Exception):
55 | """Raised when attempting to select an instance that does not exist"""
56 | pass
57 |
58 | class NothingSelectedError(Exception):
59 | """Raised when attempting to access an object without first selecting it"""
60 | pass
61 |
62 | class NothingSelectedSetError(Exception):
63 | """Raised when attempting to access an object
64 | without first selecting it"""
65 | pass
66 |
67 | class InvalidSelectionError(Exception):
68 | """Raised when selection argument fails to match anything"""
69 | pass
70 |
71 | class SessionExpired(Exception):
72 | """Raised when the session has expired"""
73 | pass
74 |
75 | class LoadSkipSettingError(Exception):
76 | """Raised when one or more settings are absent in given server"""
77 | pass
78 |
79 | class InvalidPathError(Exception):
80 | """Raised when requested path is not found"""
81 | pass
82 |
83 | class RmcClient(object):
84 | """RMC client base class"""
85 | def __init__(self, url=None, username=None, password=None, sessionkey=None,
86 | proxies=None):
87 | """Initialized RmcClient
88 | :param url: redfish host name or IP address.
89 | :type url: str.
90 | :param username: user name required to login to server.
91 | :type: str.
92 | :param password: password credentials required to login.
93 | :type password: str.
94 | :param sessionkey: session key credential for current login
95 | :type sessionkey: str
96 | :param proxies: Dictionary containing protocol to proxy URL mappings
97 | :type proxies: dict
98 |
99 | """
100 | self._rest_client = redfish.rest.v1.redfish_client(
101 | base_url=url, username=username, password=password,
102 | sessionkey=sessionkey, proxies=proxies)
103 |
104 | self._get_cache = dict()
105 | self._monolith = RisMonolith(self)
106 | self._selector = None
107 | self._filter_attr = None
108 | self._filter_value = None
109 |
110 | def get_username(self):
111 | """The rest client's current user name"""
112 | return self._rest_client.get_username()
113 |
114 | def set_username(self, username):
115 | """Sets the rest client's user name
116 |
117 | :param username: user name to set for login
118 | :type username: str
119 |
120 | """
121 | self._rest_client.set_username(username)
122 |
123 | def get_password(self):
124 | """The rest client's current password"""
125 | return self._rest_client.get_password()
126 |
127 | def set_password(self, password):
128 | """Sets the rest clients's password
129 |
130 | :param password: password to set for login
131 | :type password: str
132 |
133 | """
134 | self._rest_client.set_password(password)
135 |
136 | def get_session_key(self):
137 | """The rest client's current session key"""
138 | return self._rest_client.get_session_key()
139 |
140 | def get_session_location(self):
141 | """The rest client's current session location"""
142 | return self._rest_client.get_session_location()
143 |
144 | def get_authorization_key(self):
145 | """The rest client's current authorization key"""
146 | return self._rest_client.get_authorization_key()
147 |
148 | def get_base_url(self):
149 | """The rest client's current base URL"""
150 | return self._rest_client.get_base_url()
151 | base_url = property(get_base_url, None)
152 |
153 | def get_cache_dirname(self):
154 | """The rest client's current base URL converted to path"""
155 | parts = urlparse(self.get_base_url())
156 | pathstr = '%s/%s' % (parts.netloc, parts.path)
157 | return pathstr.replace('//', '/')
158 |
159 | def get_monolith(self):
160 | """The rest client's current monolith"""
161 | return self._monolith
162 | monolith = property(get_monolith, None)
163 |
164 | def _get_selector(self):
165 | """The rest client's current selector"""
166 | return self._selector
167 |
168 | def _set_selector(self, selectorval):
169 | """Sets the rest client's selector
170 |
171 | :param selectorval: the type selection for the set operation.
172 | :type selectorval: str
173 |
174 | """
175 | self._selector = selectorval
176 | selector = property(_get_selector, _set_selector)
177 |
178 | def _get_filter_attr(self):
179 | """The rest client's current filter"""
180 | return self._filter_attr
181 |
182 | def _set_filter_attr(self, filterattr):
183 | """Sets the rest client's filter
184 |
185 | :param filterattr: the type selection for the select operation.
186 | :type filterattr: str.
187 |
188 | """
189 | self._filter_attr = filterattr
190 | filter_attr = property(_get_filter_attr, _set_filter_attr)
191 |
192 | def _get_filter_value(self):
193 | """The rest client's current filter value"""
194 | return self._filter_value
195 |
196 | def _set_filter_value(self, filterval):
197 | """Sets the rest client's filter value
198 |
199 | :param filterval: value for the property to be modified.
200 | :type filterval: str.
201 |
202 | """
203 | self._filter_value = filterval
204 | filter_value = property(_get_filter_value, _set_filter_value)
205 |
206 | def login(self):
207 | """Login using rest client"""
208 | self._rest_client.login(auth="session")
209 |
210 | def logout(self):
211 | """Logout using rest client"""
212 | self._rest_client.logout()
213 |
214 | def get(self, path, args=None, uncache=False, headers=None):
215 | """Perform a GET request on path argument.
216 |
217 | :param path: The URL to perform a GET request on.
218 | :type path: str.
219 | :param args: GET arguments.
220 | :type args: str.
221 | :returns: a RestResponse object containing the response data.
222 | :rtype: redfish.rest.v1.RestResponse.
223 | :param uncache: flag to not store the data downloaded into cache.
224 | :type uncache: boolean.
225 | :param headers: dict of headers to be appended.
226 | :type headers: dict.
227 |
228 | """
229 | resp = self._rest_client.get(path=path, args=args, headers=headers)
230 |
231 | if uncache is False:
232 | self._get_cache[path] = resp
233 |
234 | return resp
235 |
236 | def head(self, path, args=None, headers=None):
237 | """Perform a HEAD request on path argument.
238 |
239 | :param path: The URL to perform a GET request on.
240 | :type path: str.
241 | :param args: GET arguments.
242 | :type args: str.
243 | :param headers: dict of headers to be appended.
244 | :type headers: dict.
245 | :returns: a RestResponse object containing the response data.
246 | :rtype: redfish.rest.v1.RestResponse.
247 |
248 | """
249 | resp = self._rest_client.head(path=path, args=args, headers=headers)
250 | return resp
251 |
252 | def set(self, path, args=None, body=None, headers=None):
253 | """Perform a PATCH request on path argument.
254 |
255 | :param path: The URL to perform a PATCH request on.
256 | :type path: str.
257 | :param args: GET arguments.
258 | :type args: str.
259 | :param body: contents of the PATCH request.
260 | :type body: str.
261 | :param headers: list of headers to be appended.
262 | :type headers: list.
263 | :returns: a RestResponse object containing the response data.
264 | :rtype: redfish.rest.v1.RestResponse.
265 |
266 | """
267 | resp = self._rest_client.get(path=path, args=args)
268 | self._get_cache[path] = resp
269 |
270 | return self._rest_client.patch(path=path, args=args, body=body,
271 | headers=headers)
272 |
273 | def toolpost(self, path, args=None, body=None, headers=None):
274 | """Perform a POST request on path argument.
275 |
276 | :param path: The URL to perform a POST request on.
277 | :type path: str.
278 | :param args: POST arguments.
279 | :type args: str.
280 | :param body: contents of the POST request.
281 | :type body: str.
282 | :param headers: list of headers to be appended.
283 | :type headers: list.
284 | :returns: a RestResponse object containing the response data.
285 | :rtype: redfish.rest.v1.RestResponse.
286 |
287 | """
288 | resp = self._rest_client.get(path=path, args=args)
289 | self._get_cache[path] = resp
290 |
291 | return self._rest_client.post(path=path, args=args, body=body,
292 | headers=headers)
293 |
294 | def toolput(self, path, args=None, body=None, headers=None):
295 | """
296 | Perform a PUT request on path argument.
297 |
298 | :param path: The URL to perform a PUT request on.
299 | :type path: str.
300 | :param args: PUT arguments.
301 | :type args: str.
302 | :param body: contents of the PUT request.
303 | :type body: str.
304 | :param headers: list of headers to be appended.
305 | :type headers: list.
306 | :returns: a RestResponse object containing the response data.
307 | :rtype: redfish.rest.v1.RestResponse.
308 | """
309 | resp = self._rest_client.get(path=path, args=args)
310 | self._get_cache[path] = resp
311 |
312 | return self._rest_client.put(path=path, args=args, body=body,
313 | headers=headers)
314 |
315 | def tooldelete(self, path, args=None, headers=None):
316 | """Perform a PUT request on path argument.
317 |
318 | :param path: The URL to perform a DELETE request on.
319 | :type path: str.
320 | :param args: DELETE arguments.
321 | :type args: str.
322 | :param headers: list of headers to be appended.
323 | :type headers: list.
324 | :returns: a RestResponse object containing the response data.
325 | :rtype: redfish.rest.v1.RestResponse.
326 |
327 | """
328 | resp = self._rest_client.get(path=path, args=args)
329 | self._get_cache[path] = resp
330 |
331 | return self._rest_client.delete(path=path, args=args, headers=headers)
332 |
333 | class RmcConfig(AutoConfigParser):
334 | """RMC config object"""
335 | def __init__(self, filename=None):
336 | """Initialize RmcConfig
337 |
338 | :param filename: file name to be used for Rmcconfig loading.
339 | :type filename: str
340 |
341 | """
342 | AutoConfigParser.__init__(self, filename=filename)
343 | self._sectionname = 'redfish'
344 | self._configfile = filename
345 | self._ac__logdir = os.getcwd()
346 | self._ac__cache = True
347 | self._ac__url = ''
348 | self._ac__username = ''
349 | self._ac__password = ''
350 | self._ac__commit = ''
351 | self._ac__format = ''
352 | self._ac__cachedir = ''
353 | self._ac__savefile = ''
354 | self._ac__loadfile = ''
355 |
356 | def get_configfile(self):
357 | """The current configuration file"""
358 | return self._configfile
359 |
360 | def set_configfile(self, config_file):
361 | """Set the current configuration file
362 |
363 | :param config_file: file name to be used for Rmcconfig loading.
364 | :type config_file: str
365 |
366 | """
367 | self._configfile = config_file
368 |
369 | def get_logdir(self):
370 | """Get the current log directory"""
371 | return self._get('logdir')
372 |
373 | def set_logdir(self, value):
374 | """Set the current log directory
375 |
376 | :param value: current working directory for logging
377 | :type value: str
378 |
379 | """
380 | return self._set('logdir', value)
381 |
382 | def get_cache(self):
383 | """Get the config file cache status"""
384 | if isinstance(self._get('cache'), bool):
385 | return self._get('cache')
386 | else:
387 | return self._get('cache').lower() in ("yes", "true", "t", "1")
388 |
389 | def set_cache(self, value):
390 | """Get the config file cache status
391 |
392 | :param value: status of config file cache
393 | :type value: bool
394 |
395 | """
396 | return self._set('cache', value)
397 |
398 | def get_url(self):
399 | """Get the config file URL"""
400 | return self._get('url')
401 |
402 | def set_url(self, value):
403 | """Set the config file URL
404 |
405 | :param value: URL path for the config file
406 | :type value: str
407 |
408 | """
409 | return self._set('url', value)
410 |
411 | def get_username(self):
412 | """Get the config file user name"""
413 | return self._get('username')
414 |
415 | def set_username(self, value):
416 | """Set the config file user name
417 |
418 | :param value: user name for config file
419 | :type value: str
420 |
421 | """
422 | return self._set('username', value)
423 |
424 | def get_password(self):
425 | """Get the config file password"""
426 | return self._get('password')
427 |
428 | def set_password(self, value):
429 | """Set the config file password
430 |
431 | :param value: password for config file
432 | :type value: str
433 |
434 | """
435 | return self._set('password', value)
436 |
437 | def get_commit(self):
438 | """Get the config file commit status"""
439 | return self._get('commit')
440 |
441 | def set_commit(self, value):
442 | """Set the config file commit status
443 |
444 | :param value: commit status
445 | :type value: str
446 |
447 | """
448 | return self._set('commit', value)
449 |
450 | def get_format(self):
451 | """Get the config file default format"""
452 | return self._get('format')
453 |
454 | def set_format(self, value):
455 | """Set the config file default format
456 |
457 | :param value: set the config file format
458 | :type value: str
459 |
460 | """
461 | return self._set('format', value)
462 |
463 | def get_cachedir(self):
464 | """Get the config file cache directory"""
465 | return self._get('cachedir')
466 |
467 | def set_cachedir(self, value):
468 | """Set the config file cache directory
469 |
470 | :param value: config file cache directory
471 | :type value: str
472 |
473 | """
474 | return self._set('cachedir', value)
475 |
476 | def get_defaultsavefilename(self):
477 | """Get the config file default save name"""
478 | return self._get('savefile')
479 |
480 | def set_defaultsavefilename(self, value):
481 | """Set the config file default save name
482 |
483 | :param value: config file save name
484 | :type value: str
485 |
486 | """
487 | return self._set('savefile', value)
488 |
489 | def get_defaultloadfilename(self):
490 | """Get the config file default load name"""
491 | return self._get('loadfile')
492 |
493 | def set_defaultloadfilename(self, value):
494 | """Set the config file default load name
495 |
496 | :param value: name of config file to load by default
497 | :type value: str
498 |
499 | """
500 | return self._set('loadfile', value)
501 |
502 | class RmcCacheManager(object):
503 | """Manages caching/uncaching of data for RmcApp"""
504 | def __init__(self, rmc):
505 | """Initialize RmcCacheManager
506 |
507 | :param rmc: RmcApp to be managed
508 | :type rmc: RmcApp object
509 |
510 | """
511 | self._rmc = rmc
512 |
513 | class RmcFileCacheManager(RmcCacheManager):
514 | """RMC file cache manager"""
515 | def __init__(self, rmc):
516 | super(RmcFileCacheManager, self).__init__(rmc)
517 |
518 | def logout_del_function(self, url=None):
519 | """Helper function for logging out a specific URL
520 |
521 | :param url: The URL to perform a logout request on.
522 | :type url: str.
523 |
524 | """
525 | cachedir = self._rmc.config.get_cachedir()
526 | indexfn = '%s/index' % cachedir
527 | sessionlocs = []
528 |
529 | if os.path.isfile(indexfn):
530 | try:
531 | indexfh = open(indexfn, 'r')
532 | index_cache = json.load(indexfh)
533 | indexfh.close()
534 |
535 | for index in index_cache:
536 | if url:
537 | if url in index['url']:
538 | os.remove(os.path.join(cachedir, index['href']))
539 | break
540 | else:
541 | if os.path.isfile(cachedir + '/' + index['href']):
542 | monolith = open(cachedir + '/' + index['href'], 'r')
543 | data = json.load(monolith)
544 | monolith.close()
545 | for item in data:
546 | if 'login' in item and 'session_location' in \
547 | item['login']:
548 | loc = item['login']['session_location'].\
549 | split(item['login']['url'])[-1]
550 | sesurl = item['login']['url']
551 | sessionlocs.append((loc, sesurl,
552 | item['login']['session_key']))
553 |
554 | os.remove(os.path.join(cachedir, index['href']))
555 | except BaseException as excp:
556 | self._rmc.warn('Unable to read cache data %s' % excp)
557 |
558 | return sessionlocs
559 |
560 | def uncache_rmc(self):
561 | """Simple monolith uncache function"""
562 | cachedir = self._rmc.config.get_cachedir()
563 | indexfn = '%s/index' % cachedir
564 |
565 | if os.path.isfile(indexfn):
566 | try:
567 | indexfh = open(indexfn, 'r')
568 | index_cache = json.load(indexfh)
569 | indexfh.close()
570 |
571 | for index in index_cache:
572 | clientfn = index['href']
573 | self._uncache_client(clientfn)
574 | except BaseException as excp:
575 | self._rmc.warn('Unable to read cache data %s' % excp)
576 |
577 | def _uncache_client(self, cachefn):
578 | """Complex monolith uncache function
579 |
580 | :param cachefn: The cache file name.
581 | :type cachefn: str.
582 |
583 | """
584 | cachedir = self._rmc.config.get_cachedir()
585 | clientsfn = '%s/%s' % (cachedir, cachefn)
586 |
587 | if os.path.isfile(clientsfn):
588 | try:
589 | clientsfh = open(clientsfn, 'r')
590 | clients_cache = json.load(clientsfh)
591 | clientsfh.close()
592 |
593 | for client in clients_cache:
594 | if 'login' not in client:
595 | continue
596 |
597 | login_data = client['login']
598 | if 'url' not in login_data:
599 | continue
600 |
601 | rmc_client = RmcClient(
602 | username=login_data.get('username', 'Administrator'),
603 | password=login_data.get('password', None),
604 | url=login_data.get('url', None),
605 | sessionkey=login_data.get('session_key', None))
606 |
607 | rmc_client._rest_client.set_authorization_key(
608 | login_data.get('authorization_key'))
609 | rmc_client._rest_client.set_session_key(
610 | login_data.get('session_key'))
611 | rmc_client._rest_client.set_session_location(
612 | login_data.get('session_location'))
613 |
614 | if 'selector' in client:
615 | rmc_client.selector = client['selector']
616 |
617 | if 'filter_attr' in client:
618 | rmc_client.filter_attr = client['filter_attr']
619 |
620 | if 'filter_value' in client:
621 | rmc_client.filter_value = client['filter_value']
622 |
623 | getdata = client['get']
624 | rmc_client._get_cache = dict()
625 |
626 | for key in list(getdata.keys()):
627 | restreq = redfish.rest.v1.RestRequest(
628 | method='GET', path=key)
629 |
630 | getdata[key]['restreq'] = restreq
631 | rmc_client._get_cache[key] = (
632 | redfish.rest.v1.StaticRestResponse(
633 | **getdata[key]))
634 |
635 | rmc_client._monolith = RisMonolith(rmc_client)
636 | rmc_client._monolith.load_from_dict(client['monolith'])
637 | self._rmc._rmc_clients.append(rmc_client)
638 | except BaseException as excp:
639 | self._rmc.warn('Unable to read cache data %s' % excp)
640 |
641 | def cache_rmc(self):
642 | """Caching function for monolith"""
643 | if not self._rmc.config.get_cache():
644 | return
645 |
646 | cachedir = self._rmc.config.get_cachedir()
647 | if not os.path.isdir(cachedir):
648 | try:
649 | os.makedirs(cachedir)
650 | except OSError as ex:
651 | if ex.errno == errno.EEXIST:
652 | pass
653 | else:
654 | raise
655 |
656 | index_map = dict()
657 | index_cache = list()
658 |
659 | for client in self._rmc._rmc_clients:
660 | md5 = hashlib.md5()
661 | md5.update(client.get_base_url().encode('utf-8'))
662 | md5str = md5.hexdigest()
663 |
664 | index_map[client.get_base_url()] = md5str
665 | index_data = dict(url=client.get_base_url(), href='%s' % md5str,)
666 | index_cache.append(index_data)
667 |
668 | indexfh = open('%s/index' % cachedir, 'w')
669 | json.dump(index_cache, indexfh, indent=2, cls=JSONEncoder)
670 | indexfh.close()
671 |
672 | clients_cache = list()
673 |
674 | if self._rmc._rmc_clients:
675 | for client in self._rmc._rmc_clients:
676 | login_data = dict(
677 | username=client.get_username(),
678 | password=client.get_password(), url=client.get_base_url(),
679 | session_key=client.get_session_key(),
680 | session_location=client.get_session_location(),
681 | authorization_key=client.get_authorization_key())
682 |
683 | clients_cache.append(
684 | dict(selector=client.selector, login=login_data,
685 | filter_attr=client._filter_attr,
686 | filter_value=client._filter_value,
687 | monolith=client.monolith, get=client._get_cache))
688 |
689 | clientsfh = open('%s/%s' % (cachedir,
690 | index_map[client.get_base_url()]), 'w')
691 |
692 | json.dump(clients_cache, clientsfh, indent=2, cls=JSONEncoder)
693 | clientsfh.close()
694 |
--------------------------------------------------------------------------------
/src/redfish/rest/v1.py:
--------------------------------------------------------------------------------
1 | # Copyright Notice:
2 | # Copyright 2016-2021 DMTF. All rights reserved.
3 | # License: BSD 3-Clause License. For full text see link:
4 | # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md
5 |
6 | # -*- coding: utf-8 -*-
7 | """Helper module for working with REST technology."""
8 |
9 | #---------Imports---------
10 |
11 | import sys
12 | import time
13 | import gzip
14 | import json
15 | import base64
16 | import logging
17 | import warnings
18 | import re
19 | import requests
20 | import requests_unixsocket
21 | from redfish.messages import *
22 |
23 | from collections import (OrderedDict)
24 |
25 | from urllib.parse import urlparse, urlencode, quote
26 | from io import StringIO
27 |
28 | from requests_toolbelt import MultipartEncoder
29 |
30 | # Many services come with self-signed certificates and will remain as such; need to suppress warnings for this
31 | from requests.packages.urllib3.exceptions import InsecureRequestWarning
32 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
33 |
34 | #---------End of imports---------
35 |
36 | #---------Debug logger---------
37 |
38 | LOGGER = logging.getLogger(__name__)
39 |
40 | #---------End of debug logger---------
41 |
42 | class RetriesExhaustedError(Exception):
43 | """Raised when retry attempts have been exhausted."""
44 | pass
45 |
46 | class InvalidCredentialsError(Exception):
47 | """Raised when invalid credentials have been provided."""
48 | pass
49 |
50 | class SessionCreationError(Exception):
51 | """Raised when a session could not be created."""
52 | pass
53 |
54 | class ServerDownOrUnreachableError(Exception):
55 | """Raised when server is unreachable."""
56 | def __init__(self,message,*,response=None):
57 | super().__init__(message)
58 | self.response = response
59 |
60 | class DecompressResponseError(Exception):
61 | """Raised when decompressing response failed."""
62 | pass
63 |
64 | class JsonDecodingError(Exception):
65 | """Raised when the JSON response data is malformed."""
66 | pass
67 |
68 | class BadRequestError(Exception):
69 | """Raised when bad request made to server."""
70 | pass
71 |
72 | class RisObject(dict):
73 | """Converts a JSON/Rest dict into a object so you can use .property
74 | notation"""
75 | __getattr__ = dict.__getitem__
76 |
77 | def __init__(self, d):
78 | """Initialize RisObject
79 |
80 | :param d: dictionary to be parsed
81 | :type d: dict
82 |
83 | """
84 | super(RisObject, self).__init__()
85 | self.update(**dict((k, self.parse(value)) \
86 | for k, value in d.items()))
87 |
88 | @classmethod
89 | def parse(cls, value):
90 | """Parse for RIS value
91 |
92 | :param cls: class referenced from class method
93 | :type cls: RisObject
94 | :param value: value to be parsed
95 | :type value: data type
96 | :returns: returns parsed value
97 |
98 | """
99 | if isinstance(value, dict):
100 | return cls(value)
101 | elif isinstance(value, list):
102 | return [cls.parse(i) for i in value]
103 | else:
104 | return value
105 |
106 | class RestRequest(object):
107 | """Holder for Request information"""
108 | def __init__(self, path, method='GET', body=''):
109 | """Initialize RestRequest
110 |
111 | :param path: path within tree
112 | :type path: str
113 | :param method: method to be implemented
114 | :type method: str
115 | :param body: body payload for the rest call
116 | :type body: dict
117 |
118 | """
119 | self._path = path
120 | self._body = body
121 | self._method = method
122 |
123 | def _get_path(self):
124 | """Return object path"""
125 | return self._path
126 |
127 | path = property(_get_path, None)
128 |
129 | def _get_method(self):
130 | """Return object method"""
131 | return self._method
132 |
133 | method = property(_get_method, None)
134 |
135 | def _get_body(self):
136 | """Return object body"""
137 | return self._body
138 |
139 | body = property(_get_body, None)
140 |
141 | def __str__(self):
142 | """Format string"""
143 | strvars = dict(body=self.body, method=self.method, path=self.path)
144 |
145 | # set None to '' for strings
146 | if not strvars['body']:
147 | strvars['body'] = ''
148 |
149 | try:
150 | strvars['body'] = str(str(self._body))
151 | except BaseException:
152 | strvars['body'] = ''
153 |
154 | return "%(method)s %(path)s\n\n%(body)s" % strvars
155 |
156 | class RestResponse(object):
157 | """Returned by Rest requests"""
158 | def __init__(self, rest_request, http_response):
159 | """Initialize RestResponse
160 |
161 | :params rest_request: Holder for request information
162 | :type rest_request: RestRequest object
163 | :params http_response: Response from HTTP
164 | :type http_response: requests.Response
165 |
166 | """
167 | self._read = None
168 | self._status = None
169 | self._session_key = None
170 | self._session_location = None
171 | self._task_location = None
172 | self._rest_request = rest_request
173 | self._http_response = http_response
174 |
175 | if http_response is not None:
176 | self._read = http_response.content
177 | self._status = http_response.status_code
178 |
179 | @property
180 | def read(self):
181 | """Property for accessing raw content as an array of bytes (unless overridden)
182 |
183 | TODO: Need to review usage elsewhwere; by default _read is an array of bytes, but applying a new value with a
184 | setter routine will make it a string. We might want to consider deprecating the setters.
185 | """
186 | return self._read
187 |
188 | @read.setter
189 | def read(self, read):
190 | """Property for setting _read
191 |
192 | :param read: The data to set to read.
193 | :type read: str.
194 |
195 | """
196 | if read is not None:
197 | if isinstance(read, dict):
198 | read = json.dumps(read, indent=4)
199 | self._read = read
200 |
201 | def getheaders(self):
202 | """Property for accessing the headers"""
203 |
204 | # Backwards compatibility: requests simply uses a dictionary, but older versions of this library returned a list of tuples
205 | headers = []
206 | for header in self._http_response.headers:
207 | headers.append((header, self._http_response.headers[header]))
208 | return headers
209 |
210 | def getheader(self, name):
211 | """Property for accessing an individual header
212 |
213 | :param name: The header name to retrieve.
214 | :type name: str.
215 | :returns: returns a header from HTTP response
216 |
217 | """
218 | return self._http_response.headers.get(name.lower(), None)
219 |
220 | def json(self, newdict):
221 | """Property for setting JSON data
222 |
223 | :param newdict: The string data to set as JSON data.
224 | :type newdict: str.
225 |
226 | """
227 | self._read = json.dumps(newdict, indent=4)
228 |
229 | @property
230 | def text(self):
231 | """Property for accessing the data as an unparsed string"""
232 | if isinstance(self.read, str):
233 | value = self.read
234 | else:
235 | value = self.read.decode("utf-8", "ignore")
236 | return value
237 |
238 | @text.setter
239 | def text(self, value):
240 | """Property for setting text unparsed data
241 |
242 | :param value: The unparsed data to set as text.
243 | :type value: str.
244 |
245 | """
246 | self.read = value
247 |
248 | @property
249 | def dict(self):
250 | """Property for accessing the data as an dict"""
251 | try:
252 | if len(self.text) == 0:
253 | # No response body; return empty dict instead to avoid exceptions
254 | # No response bodies can be valid in many cases (especially 4XX and 5XX responses)
255 | return {}
256 | return json.loads(self.text)
257 | except:
258 | if self.status == 500:
259 | # Make an allowance for 500 status codes
260 | # Depending on the reason for the error, it's possible the web server may not be able to support Redfish handling
261 | return {}
262 | str = "Service responded with invalid JSON at URI {}\n{}".format(
263 | self._rest_request.path, self.text)
264 | LOGGER.error(str)
265 | raise JsonDecodingError(str) from None
266 |
267 | @property
268 | def obj(self):
269 | """Property for accessing the data as an object"""
270 | return RisObject.parse(self.dict)
271 |
272 | @property
273 | def status(self):
274 | """Property for accessing the status code"""
275 | return self._status
276 |
277 | @property
278 | def session_key(self):
279 | """Property for accessing the saved session key"""
280 | if self._session_key:
281 | return self._session_key
282 |
283 | self._session_key = self.getheader('x-auth-token')
284 | return self._session_key
285 |
286 | @property
287 | def session_location(self):
288 | """Property for accessing the saved session location"""
289 | if self._session_location:
290 | return self._session_location
291 | if self.status not in [200, 201, 202, 204]:
292 | return None
293 |
294 | self._session_location = self.getheader('location')
295 | if self._session_location is None:
296 | warnings.warn("Service incorrectly did not provide the 'Location' response header when creating a session; attempting to "
297 | "get the session location from the response body. Contact your vendor.")
298 | self._session_location = self.dict["@odata.id"]
299 | return self._session_location
300 |
301 | @property
302 | def task_location(self):
303 | """Return if we're a PATCH/POST in with a task link """
304 | if self._task_location:
305 | return self._task_location
306 |
307 | self._task_location = self.getheader('location')
308 | return self._task_location
309 |
310 | @property
311 | def is_processing(self):
312 | """Check if we're a PATCH/POST in progress """
313 | return self.status == 202
314 |
315 | @property
316 | def retry_after(self):
317 | """Retry After header"""
318 | retry_after = self.getheader('retry-after')
319 | if retry_after is not None:
320 | # Convert to int for ease of use by callers
321 | try:
322 | retry_after = int(retry_after)
323 | except:
324 | retry_after = 5
325 | return retry_after
326 |
327 | def monitor(self, context):
328 | """Function to process Task, used on an action or POST/PATCH that returns 202"""
329 | my_href = self.task_location
330 | if self.is_processing:
331 | if my_href:
332 | my_content = context.get(my_href, None)
333 | return my_content
334 | elif my_href is None:
335 | raise ValueError('We are processing a 202, but provide no location')
336 | return self
337 |
338 | @property
339 | def request(self):
340 | """Property for accessing the saved http request"""
341 | return self._rest_request
342 |
343 | def __str__(self):
344 | """Class string formatter"""
345 | headerstr = ''
346 | for header in self.getheaders():
347 | headerstr += '%s %s\n' % (header[0], header[1])
348 |
349 | return "%(status)s\n%(headerstr)s\n\n%(body)s" % \
350 | {'status': self.status, 'headerstr': headerstr,
351 | 'body': self.text}
352 |
353 | class JSONEncoder(json.JSONEncoder):
354 | """JSON Encoder class"""
355 | def default(self, obj):
356 | """Set defaults in JSON encoder class
357 |
358 | :param obj: object to be encoded into JSON.
359 | :type obj: RestResponse object.
360 | :returns: returns a JSON ordered dict
361 |
362 | """
363 | if isinstance(obj, RestResponse):
364 | jsondict = OrderedDict()
365 | jsondict['Status'] = obj.status
366 | jsondict['Headers'] = list()
367 |
368 | for hdr in obj.getheaders():
369 | headerd = dict()
370 | headerd[hdr[0]] = hdr[1]
371 | jsondict['Headers'].append(headerd)
372 |
373 | if obj.text:
374 | jsondict['Content'] = obj.dict
375 |
376 | return jsondict
377 |
378 | return json.JSONEncoder.default(self, obj)
379 |
380 | class JSONDecoder(json.JSONDecoder):
381 | """Custom JSONDecoder that understands our types"""
382 | def decode(self, json_string):
383 | """Decode JSON string
384 |
385 | :param json_string: The JSON string to be decoded into usable data.
386 | :type json_string: str.
387 | :returns: returns a parsed dict
388 |
389 | """
390 | parsed_dict = super(JSONDecoder, self).decode(json_string)
391 | return parsed_dict
392 |
393 | class StaticRestResponse(RestResponse):
394 | """A RestResponse object used when data is being cached."""
395 | def __init__(self, **kwargs):
396 | restreq = None
397 |
398 | if 'restreq' in kwargs:
399 | restreq = kwargs['restreq']
400 |
401 | super(StaticRestResponse, self).__init__(restreq, None)
402 |
403 | if 'Status' in kwargs:
404 | self._status = kwargs['Status']
405 |
406 | if 'Headers' in kwargs:
407 | self._headers = kwargs['Headers']
408 |
409 | if 'session_key' in kwargs:
410 | self._session_key = kwargs['session_key']
411 |
412 | if 'session_location' in kwargs:
413 | self._session_location = kwargs['session_location']
414 |
415 | if 'Content' in kwargs:
416 | content = kwargs['Content']
417 |
418 | if isinstance(content, str):
419 | self._read = content
420 | else:
421 | self._read = json.dumps(content)
422 | else:
423 | self._read = ''
424 |
425 | def getheaders(self):
426 | """Function for accessing the headers"""
427 | returnlist = list()
428 |
429 | if isinstance(self._headers, dict):
430 | for key, value in self._headers.items():
431 | returnlist.append((key, value))
432 | else:
433 | for item in self._headers:
434 | returnlist.append(item)
435 |
436 | return returnlist
437 |
438 | def getheader(self, name):
439 | """Property for accessing an individual header
440 |
441 | :param name: The header name to retrieve.
442 | :type name: str.
443 | :returns: returns a header from HTTP response
444 | """
445 | returnheader = None
446 |
447 | if isinstance(self._headers, dict):
448 | for key, value in self._headers.items():
449 | if key.lower() == name.lower():
450 | returnheader = self._headers[key]
451 | break
452 | else:
453 | for item in self._headers:
454 | if item[0].lower() == name.lower():
455 | returnheader = item[1]
456 | break
457 |
458 | return returnheader
459 |
460 | class AuthMethod(object):
461 | """AUTH Method class"""
462 | BASIC = 'basic'
463 | SESSION = 'session'
464 |
465 | class RestClientBase(object):
466 | """Base class for RestClients"""
467 |
468 | def __init__(self, base_url, username=None, password=None,
469 | default_prefix='/redfish/v1/', sessionkey=None,
470 | capath=None, cafile=None, timeout=None,
471 | max_retry=None, proxies=None, check_connectivity=True,
472 | https_adapter = None):
473 | """Initialization of the base class RestClientBase
474 |
475 | :param base_url: The URL of the remote system
476 | :type base_url: str
477 | :param username: The user name used for authentication
478 | :type username: str
479 | :param password: The password used for authentication
480 | :type password: str
481 | :param default_prefix: The default root point
482 | :type default_prefix: str
483 | :param sessionkey: session key for the current login of base_url
484 | :type sessionkey: str
485 | :param capath: Path to a directory containing CA certificates
486 | :type capath: str
487 | :param cafile: Path to a file of CA certs
488 | :type cafile: str
489 | :param timeout: Timeout in seconds for the initial connection
490 | :type timeout: int
491 | :param max_retry: Number of times a request will retry after a timeout
492 | :type max_retry: int
493 | :param proxies: Dictionary containing protocol to proxy URL mappings
494 | :type proxies: dict
495 | :param check_connectivity: A boolean to determine whether the client immediately checks for
496 | connectivity to the base_url or not.
497 | :type check_connectivity: bool
498 | :type https_adapter: requests.adpaters.HTTPAdapter
499 | """
500 |
501 | self.__base_url = base_url.rstrip('/')
502 | self.__username = username
503 | self.__password = password
504 | self.__session_key = sessionkey
505 | self.__authorization_key = None
506 | self.__session_location = None
507 | if self.__base_url.startswith('http+unix://'):
508 | self._session = requests_unixsocket.Session()
509 | else:
510 | self._session = requests.Session()
511 | if https_adapter:
512 | self._session.mount('https://',https_adapter)
513 | self._timeout = timeout
514 | self._max_retry = max_retry if max_retry is not None else 10
515 | self._proxies = proxies
516 | self.login_url = None
517 | self.default_prefix = default_prefix
518 | self.capath = capath
519 | self.cafile = cafile
520 |
521 | if check_connectivity:
522 | self.get_root_object()
523 |
524 | def __enter__(self):
525 | self.login()
526 | return self
527 |
528 | def __exit__(self, exc_type, exc_value, exc_traceback):
529 | self.logout()
530 |
531 | def get_username(self):
532 | """Return used user name"""
533 | return self.__username
534 |
535 | def set_username(self, username):
536 | """Set user name
537 |
538 | :param username: The user name to be set.
539 | :type username: str
540 |
541 | """
542 | self.__username = username
543 |
544 | def get_password(self):
545 | """Return used password"""
546 | return self.__password
547 |
548 | def set_password(self, password):
549 | """Set password
550 |
551 | :param password: The password to be set.
552 | :type password: str
553 |
554 | """
555 | self.__password = password
556 |
557 | def get_base_url(self):
558 | """Return used URL"""
559 | return self.__base_url
560 |
561 | def set_base_url(self, url):
562 | """Set based URL
563 |
564 | :param url: The URL to be set.
565 | :type url: str
566 |
567 | """
568 | self.__base_url = url.rstrip('/')
569 |
570 | def get_session_key(self):
571 | """Return session key"""
572 | return self.__session_key
573 |
574 | def set_session_key(self, session_key):
575 | """Set session key
576 |
577 | :param session_key: The session_key to be set.
578 | :type session_key: str
579 |
580 | """
581 | self.__session_key = session_key
582 |
583 | def get_session_location(self):
584 | """Return session location"""
585 | return self.__session_location
586 |
587 | def set_session_location(self, session_location):
588 | """Set session location
589 |
590 | :param session_location: The session_location to be set.
591 | :type session_location: str
592 |
593 | """
594 | self.__session_location = session_location
595 |
596 | def get_authorization_key(self):
597 | """Return authorization key"""
598 | return self.__authorization_key
599 |
600 | def set_authorization_key(self, authorization_key):
601 | """Set authorization key
602 |
603 | :param authorization_key: The authorization_key to be set.
604 | :type authorization_key: str
605 |
606 | """
607 | self.__authorization_key = authorization_key
608 |
609 | def get_root_object(self):
610 | """Perform an initial get and store the result"""
611 | try:
612 | resp = self.get(self.default_prefix)
613 | except Exception as excp:
614 | raise excp
615 |
616 | if resp.status == 401 and self.__authorization_key is None and self.__session_key is None:
617 | # Workaround where the service incorrectly rejects access to service
618 | # root when no credentials are provided
619 | warnings.warn("Service incorrectly responded with HTTP 401 Unauthorized for the service root. Contact your vendor.")
620 | self.root = {}
621 | self.root_resp = resp
622 | return
623 | if resp.status == 503:
624 | raise ServerDownOrUnreachableError("Service is busy, " \
625 | "return code: %d" % resp.status,response=resp)
626 | if resp.status != 200:
627 | raise ServerDownOrUnreachableError("Service not able to provide the service root, " \
628 | "return code: %d" % resp.status,response=resp)
629 |
630 | content = resp.text
631 |
632 | try:
633 | root_data = json.loads(content)
634 | except:
635 | str = 'Service responded with invalid JSON at URI {}\n{}'.format(
636 | self.default_prefix, content)
637 | LOGGER.error(str)
638 | raise JsonDecodingError(str) from None
639 |
640 | self.root = RisObject.parse(root_data)
641 | self.root_resp = resp
642 |
643 | def get(self, path, args=None, headers=None, timeout=None, max_retry=None):
644 | """Perform a GET request
645 |
646 | :param path: The URI to access
647 | :type path: str
648 | :param args: The query parameters to provide with the request
649 | :type args: dict, optional
650 | :param headers: Additional HTTP headers to provide in the request
651 | :type headers: dict, optional
652 | :param timeout: Timeout in seconds for the initial connection for this specific request
653 | :type timeout: int, optional
654 | :param max_retry: Number of times a request will retry after a timeout for this specific request
655 | :type max_retry: int, optional
656 | :returns: returns a rest request with method 'Get'
657 |
658 | """
659 | try:
660 | return self._rest_request(path, method='GET', args=args,
661 | headers=headers, timeout=timeout, max_retry=max_retry)
662 | except ValueError:
663 | str = "Service responded with invalid JSON at URI {}".format(path)
664 | LOGGER.error(str)
665 | raise JsonDecodingError(str) from None
666 |
667 | def head(self, path, args=None, headers=None, timeout=None, max_retry=None):
668 | """Perform a HEAD request
669 |
670 | :param path: The URI to access
671 | :type path: str
672 | :param args: The query parameters to provide with the request
673 | :type args: dict, optional
674 | :param headers: Additional HTTP headers to provide in the request
675 | :type headers: dict, optional
676 | :param timeout: Timeout in seconds for the initial connection for this specific request
677 | :type timeout: int, optional
678 | :param max_retry: Number of times a request will retry after a timeout for this specific request
679 | :type max_retry: int, optional
680 | :returns: returns a rest request with method 'Head'
681 |
682 | """
683 | return self._rest_request(path, method='HEAD', args=args,
684 | headers=headers, timeout=timeout, max_retry=max_retry)
685 |
686 | def post(self, path, args=None, body=None, headers=None, timeout=None, max_retry=None):
687 | """Perform a POST request
688 |
689 | :param path: The URI to access
690 | :type path: str
691 | :param args: The query parameters to provide with the request
692 | :type args: dict, optional
693 | :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request
694 | :type body: dict or list or bytes or str, optional
695 | :param headers: Additional HTTP headers to provide in the request
696 | :type headers: dict, optional
697 | :param timeout: Timeout in seconds for the initial connection for this specific request
698 | :type timeout: int, optional
699 | :param max_retry: Number of times a request will retry after a timeout for this specific request
700 | :type max_retry: int, optional
701 | :returns: returns a rest request with method 'Post'
702 |
703 | """
704 | return self._rest_request(path, method='POST', args=args, body=body,
705 | headers=headers, timeout=timeout, max_retry=max_retry)
706 |
707 | def put(self, path, args=None, body=None, headers=None, timeout=None, max_retry=None):
708 | """Perform a PUT request
709 |
710 | :param path: The URI to access
711 | :type path: str
712 | :param args: The query parameters to provide with the request
713 | :type args: dict, optional
714 | :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request
715 | :type body: dict or list or bytes or str, optional
716 | :param headers: Additional HTTP headers to provide in the request
717 | :type headers: dict, optional
718 | :param timeout: Timeout in seconds for the initial connection for this specific request
719 | :type timeout: int, optional
720 | :param max_retry: Number of times a request will retry after a timeout for this specific request
721 | :type max_retry: int, optional
722 | :returns: returns a rest request with method 'Put'
723 |
724 | """
725 | return self._rest_request(path, method='PUT', args=args, body=body,
726 | headers=headers, timeout=timeout, max_retry=max_retry)
727 |
728 | def patch(self, path, args=None, body=None, headers=None, timeout=None, max_retry=None):
729 | """Perform a PATCH request
730 |
731 | :param path: The URI to access
732 | :type path: str
733 | :param args: The query parameters to provide with the request
734 | :type args: dict, optional
735 | :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request
736 | :type body: dict or list or bytes or str, optional
737 | :param headers: Additional HTTP headers to provide in the request
738 | :type headers: dict, optional
739 | :param timeout: Timeout in seconds for the initial connection for this specific request
740 | :type timeout: int, optional
741 | :param max_retry: Number of times a request will retry after a timeout for this specific request
742 | :type max_retry: int, optional
743 | :returns: returns a rest request with method 'Patch'
744 |
745 | """
746 | return self._rest_request(path, method='PATCH', args=args, body=body,
747 | headers=headers, timeout=timeout, max_retry=max_retry)
748 |
749 | def delete(self, path, args=None, headers=None, timeout=None, max_retry=None):
750 | """Perform a DELETE request
751 |
752 | :param path: The URI to access
753 | :type path: str
754 | :param args: The query parameters to provide with the request
755 | :type args: dict, optional
756 | :param headers: Additional HTTP headers to provide in the request
757 | :type headers: dict, optional
758 | :param timeout: Timeout in seconds for the initial connection for this specific request
759 | :type timeout: int, optional
760 | :param max_retry: Number of times a request will retry after a timeout for this specific request
761 | :type max_retry: int, optional
762 | :returns: returns a rest request with method 'Delete'
763 |
764 | """
765 | return self._rest_request(path, method='DELETE', args=args,
766 | headers=headers, timeout=timeout, max_retry=max_retry)
767 |
768 | def _get_req_headers(self, headers=None):
769 | """Get the request headers
770 |
771 | :param headers: additional headers to be utilized
772 | :type headers: dict
773 | :returns: returns headers
774 |
775 | """
776 | headers = headers if isinstance(headers, dict) else dict()
777 |
778 | if self.__session_key:
779 | headers['X-Auth-Token'] = self.__session_key
780 | elif self.__authorization_key:
781 | headers['Authorization'] = self.__authorization_key
782 |
783 | headers_keys = set(k.lower() for k in headers)
784 | if 'accept' not in headers_keys:
785 | headers['Accept'] = '*/*'
786 |
787 | return headers
788 |
789 | def _rest_request(self, path, method='GET', args=None, body=None,
790 | headers=None, allow_redirects=True, timeout=None, max_retry=None):
791 | """Rest request main function
792 |
793 | :param path: The URI to access
794 | :type path: str
795 | :param method: The HTTP method to invoke on the URI; GET if not provided
796 | :type method: str, optional
797 | :param args: The query parameters to provide with the request
798 | :type args: dict, optional
799 | :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request
800 | :type body: dict or list or bytes or str, optional
801 | :param headers: Additional HTTP headers to provide in the request
802 | :type headers: dict, optional
803 | :param allow_redirects: Controls whether redirects are followed
804 | :type allow_redirects: bool, optional
805 | :param timeout: Timeout in seconds for the initial connection for this specific request
806 | :type timeout: int, optional
807 | :param max_retry: Number of times a request will retry after a timeout for this specific request
808 | :type max_retry: int, optional
809 | :returns: returns a RestResponse object
810 |
811 | """
812 | if timeout is None:
813 | timeout = self._timeout
814 |
815 | if max_retry is None:
816 | max_retry = self._max_retry
817 |
818 | headers = self._get_req_headers(headers)
819 | reqpath = path.replace('//', '/')
820 |
821 | if body is not None:
822 | if isinstance(body, dict) or isinstance(body, list):
823 | if headers.get('Content-Type', None) == 'multipart/form-data':
824 | # Body contains part values, either as
825 | # - dict (where key is part name, and value is string)
826 | # - list of tuples (if the order is important)
827 | # - dict (where values are tuples as they would
828 | # be provided to requests' `files` parameter)
829 | # See https://toolbelt.readthedocs.io/en/latest/uploading-data.html#requests_toolbelt.multipart.encoder.MultipartEncoder
830 | #
831 | # Redfish specification requires two parts:
832 | # (1) UpdateParameters (JSON formatted,
833 | # adhering to the UpdateService Schema)
834 | # (2) UpdateFile (binary file to use for this update)
835 | #
836 | # The third part is optional: OemXXX
837 | encoder = MultipartEncoder(body)
838 | body = encoder.to_string()
839 |
840 | # Overwrite Content-Type, because we have to include
841 | # the boundary that the encoder generated.
842 | # Will be of the form: "multipart/form-data; boundary=abc'
843 | # where the boundary value is a UUID.
844 | headers['Content-Type'] = encoder.content_type
845 | else:
846 | headers['Content-Type'] = 'application/json'
847 | body = json.dumps(body)
848 | elif isinstance(body, bytes):
849 | headers['Content-Type'] = 'application/octet-stream'
850 | body = body
851 | else:
852 | headers['Content-Type'] = 'application/x-www-form-urlencoded'
853 | body = urlencode(body)
854 |
855 | if method == 'PUT':
856 | resp = self._rest_request(path=path, timeout=timeout, max_retry=max_retry)
857 |
858 | try:
859 | if resp.getheader('content-encoding') == 'gzip':
860 | buf = StringIO()
861 | gfile = gzip.GzipFile(mode='wb', fileobj=buf)
862 |
863 | try:
864 | gfile.write(str(body))
865 | finally:
866 | gfile.close()
867 |
868 | compresseddata = buf.getvalue()
869 | if compresseddata:
870 | data = bytearray()
871 | data.extend(memoryview(compresseddata))
872 | body = data
873 | except BaseException as excp:
874 | LOGGER.error('Error occur while compressing body: %s', excp)
875 | raise
876 |
877 | query_str = None
878 | if args:
879 | if method == 'GET':
880 | # Workaround for this: https://github.com/psf/requests/issues/993
881 | # Redfish supports some query parameters without using '=', which is apparently against HTML5
882 | none_list = []
883 | args_copy = {}
884 | for query in args:
885 | if args[query] is None:
886 | none_list.append(query)
887 | else:
888 | args_copy[query] = args[query]
889 | query_str = urlencode(args_copy, quote_via=quote, safe="/?:!$'()*+,;\\=")
890 | for query in none_list:
891 | if len(query_str) == 0:
892 | query_str += query
893 | else:
894 | query_str += '&' + query
895 | elif method == 'PUT' or method == 'POST' or method == 'PATCH':
896 | LOGGER.warning('For POST, PUT and PATCH methods, the provided "args" parameter "{}" is ignored.'
897 | .format(args))
898 | if not body:
899 | LOGGER.warning('Use the "body" parameter to supply the request payload.')
900 |
901 | restreq = RestRequest(reqpath, method=method, body=body)
902 |
903 | attempts = 0
904 | restresp = None
905 | cause_exception = None
906 | while attempts <= max_retry:
907 | if LOGGER.isEnabledFor(logging.DEBUG):
908 | headerstr = ''
909 | if headers is not None:
910 | for header in headers:
911 | if header.lower() == "authorization":
912 | headerstr += '\t{}: \n'.format(header)
913 | else:
914 | headerstr += '\t{}: {}\n'.format(header, headers[header])
915 | try:
916 | logbody = 'No request body'
917 | if restreq.body:
918 | if restreq.body[0] == '{':
919 | # Mask password properties
920 | # NOTE: If the password itself contains a double quote, it will not redact the entire password
921 | logbody = re.sub(r'"Password"\s*:\s*".*?"', '"Password": ""', restreq.body)
922 | else:
923 | raise ValueError('Body of message is binary')
924 | LOGGER.debug('HTTP REQUEST (%s) for %s:\nHeaders:\n%s\nBody: %s\n'% \
925 | (restreq.method, restreq.path, headerstr, logbody))
926 | except:
927 | LOGGER.debug('HTTP REQUEST (%s) for %s:\nHeaders:\n%s\nBody: %s\n'% \
928 | (restreq.method, restreq.path, headerstr, 'binary body'))
929 | attempts = attempts + 1
930 | LOGGER.info('Attempt %s of %s', attempts, path)
931 |
932 | try:
933 | if sys.version_info < (3, 3):
934 | inittime = time.clock()
935 | else:
936 | inittime = time.perf_counter()
937 |
938 | # TODO: Migration to requests lost the "CA directory" capability; need to revisit
939 | verify = False
940 | if self.cafile:
941 | verify = self.cafile
942 | resp = self._session.request(method.upper(), "{}{}".format(self.__base_url, reqpath), data=body,
943 | headers=headers, timeout=timeout, allow_redirects=allow_redirects,
944 | verify=verify, proxies=self._proxies, params=query_str)
945 |
946 | if sys.version_info < (3, 3):
947 | endtime = time.clock()
948 | else:
949 | endtime = time.perf_counter()
950 | LOGGER.info('Response Time for %s to %s: %s seconds.' %
951 | (method, reqpath, str(endtime-inittime)))
952 |
953 | restresp = RestResponse(restreq, resp)
954 | except Exception as excp:
955 | if not cause_exception:
956 | cause_exception = excp
957 | LOGGER.info('Retrying %s [%s]'% (path, excp))
958 | time.sleep(1)
959 |
960 | continue
961 | else:
962 | break
963 |
964 | if attempts <= self._max_retry:
965 | if LOGGER.isEnabledFor(logging.DEBUG):
966 | headerstr = ''
967 |
968 | if restresp is not None:
969 | for header in restresp.getheaders():
970 | headerstr += '\t' + header[0] + ': ' + header[1] + '\n'
971 |
972 | try:
973 | try:
974 | restrespstr = json.dumps(json.loads(restresp.read), indent=4)
975 | except:
976 | restrespstr = restresp.read
977 | LOGGER.debug('HTTP RESPONSE for %s:\nCode: %s\n\nHeaders:\n' \
978 | '%s\nBody Response of %s:\n%s\n'%\
979 | (restresp.request.path,
980 | str(restresp._http_response.status_code)+ ' ' + \
981 | restresp._http_response.reason,
982 | headerstr, restresp.request.path, restrespstr))
983 | except:
984 | LOGGER.debug('HTTP RESPONSE:\nCode:%s', restresp)
985 | else:
986 | LOGGER.debug('HTTP RESPONSE: ')
987 |
988 | return restresp
989 | else:
990 | raise RetriesExhaustedError() from cause_exception
991 |
992 | def login(self, username=None, password=None, auth=AuthMethod.SESSION, headers=None):
993 | """Login and start a REST session. Remember to call logout() when"""
994 | """ you are done.
995 |
996 | :param username: the user name.
997 | :type username: str.
998 | :param password: the password.
999 | :type password: str.
1000 | :param auth: authentication method
1001 | :type auth: object/instance of class AuthMethod
1002 | :param headers: Additional HTTP headers to provide in the request
1003 | :type headers: dict, optional
1004 |
1005 | """
1006 | if getattr(self, "root_resp", None) is None:
1007 | self.get_root_object()
1008 |
1009 | self.__username = username if username else self.__username
1010 | self.__password = password if password else self.__password
1011 |
1012 | headers = headers if headers else {}
1013 |
1014 | if auth == AuthMethod.BASIC:
1015 | auth_key = base64.b64encode(('%s:%s' % (self.__username,
1016 | self.__password)).encode('utf-8')).decode('utf-8')
1017 | self.__authorization_key = 'Basic %s' % auth_key
1018 |
1019 | headers['Authorization'] = self.__authorization_key
1020 |
1021 | respvalidate = self._rest_request(self.login_url, headers=headers)
1022 |
1023 | if respvalidate.status == 401:
1024 | # Invalid credentials supplied
1025 | raise InvalidCredentialsError('HTTP 401 Unauthorized returned: Invalid credentials supplied')
1026 | elif auth == AuthMethod.SESSION:
1027 | data = dict()
1028 | data['UserName'] = self.__username
1029 | data['Password'] = self.__password
1030 |
1031 | resp = self._rest_request(self.login_url, method="POST",body=data,
1032 | headers=headers, allow_redirects=False)
1033 |
1034 | LOGGER.info('Login returned code %s: %s', resp.status, resp.text)
1035 |
1036 | self.__session_key = resp.session_key
1037 | self.__session_location = resp.session_location
1038 |
1039 | message_item = search_message(resp, "Base", "PasswordChangeRequired")
1040 | if not message_item is None:
1041 | raise RedfishPasswordChangeRequiredError("Password Change Required\n", message_item["MessageArgs"][0])
1042 |
1043 | if not self.__session_key and resp.status not in [200, 201, 202, 204]:
1044 | if resp.status == 401:
1045 | # Invalid credentials supplied
1046 | raise InvalidCredentialsError('HTTP 401 Unauthorized returned: Invalid credentials supplied')
1047 | else:
1048 | # Other type of error during session creation
1049 | error_str = resp.text
1050 | try:
1051 | error_str = resp.dict["error"]["@Message.ExtendedInfo"][0]["Message"]
1052 | except:
1053 | try:
1054 | error_str = resp.dict["error"]["message"]
1055 | except:
1056 | pass
1057 | raise SessionCreationError('HTTP {}: Failed to create the session\n{}'.format(resp.status, error_str))
1058 | else:
1059 | pass
1060 |
1061 | def logout(self):
1062 | """ Logout of session. YOU MUST CALL THIS WHEN YOU ARE DONE TO FREE"""
1063 | """ UP SESSIONS"""
1064 | if self.__session_key:
1065 | session_loc = urlparse(self.__session_location).path
1066 |
1067 | resp = self.delete(session_loc)
1068 | if resp.status not in [200, 202, 204]:
1069 | raise BadRequestError("Invalid session resource: %s, "\
1070 | "return code: %d" % (session_loc, resp.status))
1071 |
1072 | LOGGER.info("User logged out: %s", resp.text)
1073 |
1074 | self.__session_key = None
1075 | self.__session_location = None
1076 | self.__authorization_key = None
1077 | self._session.close()
1078 |
1079 | class HttpClient(RestClientBase):
1080 | """A client for Rest"""
1081 | def __init__(self, base_url, username=None, password=None,
1082 | default_prefix='/redfish/v1/',
1083 | sessionkey=None, capath=None,
1084 | cafile=None, timeout=None,
1085 | max_retry=None, proxies=None, check_connectivity=True,
1086 | https_adapter=None):
1087 | """Initialize HttpClient
1088 |
1089 | :param base_url: The url of the remote system
1090 | :type base_url: str
1091 | :param username: The user name used for authentication
1092 | :type username: str
1093 | :param password: The password used for authentication
1094 | :type password: str
1095 | :param default_prefix: The default root point
1096 | :type default_prefix: str
1097 | :param sessionkey: session key for the current login of base_url
1098 | :type sessionkey: str
1099 | :param capath: Path to a directory containing CA certificates
1100 | :type capath: str
1101 | :param cafile: Path to a file of CA certs
1102 | :type cafile: str
1103 | :param timeout: Timeout in seconds for the initial connection
1104 | :type timeout: int
1105 | :param max_retry: Number of times a request will retry after a timeout
1106 | :type max_retry: int
1107 | :param proxies: Dictionary containing protocol to proxy URL mappings
1108 | :type proxies: dict
1109 | :param check_connectivity: A boolean to determine whether the client immediately checks for
1110 | connectivity to the base_url or not.
1111 | :type check_connectivity: bool
1112 | :param https_adapter session adapter for HTTPS
1113 | :type https_adapter: requests.adpaters.HTTPAdapter
1114 | """
1115 | super(HttpClient, self).__init__(base_url, username=username,
1116 | password=password, default_prefix=default_prefix,
1117 | sessionkey=sessionkey, capath=capath,
1118 | cafile=cafile, timeout=timeout,
1119 | max_retry=max_retry, proxies=proxies,
1120 | check_connectivity=check_connectivity,https_adapter=https_adapter)
1121 |
1122 | try:
1123 | self.login_url = self.root.Links.Sessions['@odata.id']
1124 | except (KeyError, AttributeError):
1125 | # While the "Links/Sessions" property is required, we can fallback
1126 | # on the URI hardened in 1.6.0 of the specification if not found
1127 | LOGGER.debug('"Links/Sessions" not found in Service Root.')
1128 | self.login_url = '/redfish/v1/SessionService/Sessions'
1129 |
1130 | def _rest_request(self, path='', method="GET", args=None, body=None,
1131 | headers=None, allow_redirects=True, timeout=None, max_retry=None):
1132 | """Rest request for HTTP client
1133 |
1134 | :param path: path within tree
1135 | :type path: str
1136 | :param method: method to be implemented
1137 | :type method: str
1138 | :param args: the arguments for method
1139 | :type args: dict
1140 | :param body: body payload for the rest call
1141 | :type body: dict
1142 | :param headers: provide additional headers
1143 | :type headers: dict
1144 | :param allow_redirects: controls whether redirects are followed
1145 | :type allow_redirects: bool
1146 | :param timeout: Timeout in seconds for the initial connection for this specific request
1147 | :type timeout: int
1148 | :param max_retry: Number of times a request will retry after a timeout for this specific request
1149 | :type max_retry: int
1150 | :returns: returns a rest request
1151 |
1152 | """
1153 | return super(HttpClient, self)._rest_request(path=path, method=method,
1154 | args=args, body=body,
1155 | headers=headers,
1156 | allow_redirects=allow_redirects, timeout=timeout, max_retry=max_retry)
1157 |
1158 | def _get_req_headers(self, headers=None, providerheader=None):
1159 | """Get the request headers for HTTP client
1160 |
1161 | :param headers: additional headers to be utilized
1162 | :type headers: dict
1163 | :returns: returns request headers
1164 |
1165 | """
1166 | headers = super(HttpClient, self)._get_req_headers(headers)
1167 | headers_keys = set(k.lower() for k in headers)
1168 | if 'odata-version' not in headers_keys:
1169 | headers['OData-Version'] = '4.0'
1170 |
1171 | return headers
1172 |
1173 | def redfish_client(base_url=None, username=None, password=None,
1174 | default_prefix='/redfish/v1/',
1175 | sessionkey=None, capath=None,
1176 | cafile=None, timeout=None,
1177 | max_retry=None, proxies=None, check_connectivity=True,
1178 | https_adapter=None):
1179 | """Create and return appropriate REDFISH client instance."""
1180 | """ Instantiates appropriate Redfish object based on existing"""
1181 | """ configuration. Use this to retrieve a pre-configured Redfish object
1182 |
1183 | :param base_url: rest host or ip address.
1184 | :type base_url: str.
1185 | :param username: user name required to login to server
1186 | :type: str
1187 | :param password: password credentials required to login
1188 | :type password: str
1189 | :param default_prefix: default root to extract tree
1190 | :type default_prefix: str
1191 | :param sessionkey: session key credential for current login
1192 | :type sessionkey: str
1193 | :param capath: Path to a directory containing CA certificates
1194 | :type capath: str
1195 | :param cafile: Path to a file of CA certs
1196 | :type cafile: str
1197 | :param timeout: Timeout in seconds for the initial connection
1198 | :type timeout: int
1199 | :param max_retry: Number of times a request will retry after a timeout
1200 | :type max_retry: int
1201 | :param proxies: Dictionary containing protocol to proxy URL mappings
1202 | :type proxies: dict
1203 | :param check_connectivity: A boolean to determine whether the client immediately checks for
1204 | connectivity to the base_url or not.
1205 | :type check_connectivity: bo#ol
1206 | :param https_adapter session adapter for HTTPS
1207 | :type https_adapter: requests.adpaters.HTTPAdapter
1208 | :returns: a client object.
1209 |
1210 | """
1211 | if "://" not in base_url:
1212 | warnings.warn("Scheme not specified for '{}'; adding 'https://'".format(base_url))
1213 | base_url = "https://" + base_url
1214 | return HttpClient(base_url=base_url, username=username, password=password,
1215 | default_prefix=default_prefix, sessionkey=sessionkey,
1216 | capath=capath, cafile=cafile, timeout=timeout,
1217 | max_retry=max_retry, proxies=proxies, check_connectivity=check_connectivity,
1218 | https_adapter=https_adapter)
1219 |
--------------------------------------------------------------------------------