Add feature sets to test with different available feature

* "full" is the default and provides all features
* "minimum" only provides boot settings
* "vmedia" is "minimum" plus virtual media and NICs

Closes-Bug: #2046153
Change-Id: I57cff90b1327104f65264d3fb87dafc89f3be521
This commit is contained in:
Dmitry Tantsur 2023-12-11 17:46:36 +01:00
parent a654a033de
commit 0e18baab40
No known key found for this signature in database
GPG Key ID: 315B2AF9FD216C60
7 changed files with 321 additions and 45 deletions

View File

@ -8,6 +8,19 @@ except that the frontend protocol is Redfish rather than IPMI. The Redfish
commands coming from the client are handled by one or more resource-specific commands coming from the client are handled by one or more resource-specific
drivers. drivers.
Feature sets
------------
The emulator can be configured with different feature sets to emulate different
hardware. The feature set is supplied either via the
``SUSHY_EMULATOR_FEATURE_SET`` configuration variable or through the
``--feature-set`` command line flag.
Supported feature sets are:
* ``minimum`` - only Systems with Boot settings and no other optional fields.
* ``vmedia`` - ``minimum`` plus Managers, VirtualMedia and EthernetInterfaces.
* ``full`` - all features implemented in the emulator.
Systems resource Systems resource
---------------- ----------------

View File

@ -0,0 +1,5 @@
---
features:
- |
Add a configuration options ``SUSHY_EMULATOR_FEATURE_SET`` to define which
resources should be available. See the documentation for more details.

View File

@ -98,6 +98,18 @@ class Application(flask.Flask):
if auth_file and not isinstance(self.wsgi_app, RedfishAuthMiddleware): if auth_file and not isinstance(self.wsgi_app, RedfishAuthMiddleware):
self.wsgi_app = RedfishAuthMiddleware(self.wsgi_app, auth_file) self.wsgi_app = RedfishAuthMiddleware(self.wsgi_app, auth_file)
feature_set = self.config.get('SUSHY_EMULATOR_FEATURE_SET', 'full')
if feature_set not in ('full', 'vmedia', 'minimum'):
raise RuntimeError(f"Invalid feature set {self.feature_set}")
@property
def feature_set(self):
return self.config.get('SUSHY_EMULATOR_FEATURE_SET', 'full')
def render_template(self, template_name, /, **params):
params.setdefault('feature_set', self.feature_set)
return flask.render_template(template_name, **params)
@property @property
@memoize.memoize() @memoize.memoize()
def systems(self): def systems(self):
@ -203,15 +215,18 @@ def all_exception_handler(message):
@app.route('/redfish/v1/') @app.route('/redfish/v1/')
@api_utils.returns_json @api_utils.returns_json
def root_resource(): def root_resource():
return flask.render_template('root.json') return app.render_template('root.json')
@app.route('/redfish/v1/Chassis') @app.route('/redfish/v1/Chassis')
@api_utils.returns_json @api_utils.returns_json
def chassis_collection_resource(): def chassis_collection_resource():
if app.feature_set != "full":
raise error.FeatureNotAvailable("Chassis")
app.logger.debug('Serving chassis list') app.logger.debug('Serving chassis list')
return flask.render_template( return app.render_template(
'chassis_collection.json', 'chassis_collection.json',
manager_count=len(app.chassis.chassis), manager_count=len(app.chassis.chassis),
chassis=app.chassis.chassis) chassis=app.chassis.chassis)
@ -220,6 +235,9 @@ def chassis_collection_resource():
@app.route('/redfish/v1/Chassis/<identity>', methods=['GET', 'PATCH']) @app.route('/redfish/v1/Chassis/<identity>', methods=['GET', 'PATCH'])
@api_utils.returns_json @api_utils.returns_json
def chassis_resource(identity): def chassis_resource(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Chassis")
chassis = app.chassis chassis = app.chassis
uuid = chassis.uuid(identity) uuid = chassis.uuid(identity)
@ -241,7 +259,7 @@ def chassis_resource(identity):
storage = [] storage = []
drives = [] drives = []
return flask.render_template( return app.render_template(
'chassis.json', 'chassis.json',
identity=identity, identity=identity,
name=chassis.name(identity), name=chassis.name(identity),
@ -272,6 +290,9 @@ def chassis_resource(identity):
@app.route('/redfish/v1/Chassis/<identity>/Thermal', methods=['GET']) @app.route('/redfish/v1/Chassis/<identity>/Thermal', methods=['GET'])
@api_utils.returns_json @api_utils.returns_json
def thermal_resource(identity): def thermal_resource(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Chassis")
chassis = app.chassis chassis = app.chassis
uuid = chassis.uuid(identity) uuid = chassis.uuid(identity)
@ -286,7 +307,7 @@ def thermal_resource(identity):
else: else:
systems = [] systems = []
return flask.render_template( return app.render_template(
'thermal.json', 'thermal.json',
chassis=identity, chassis=identity,
systems=systems systems=systems
@ -296,9 +317,12 @@ def thermal_resource(identity):
@app.route('/redfish/v1/Managers') @app.route('/redfish/v1/Managers')
@api_utils.returns_json @api_utils.returns_json
def manager_collection_resource(): def manager_collection_resource():
if app.feature_set == "minimum":
raise error.FeatureNotAvailable("Managers")
app.logger.debug('Serving managers list') app.logger.debug('Serving managers list')
return flask.render_template( return app.render_template(
'manager_collection.json', 'manager_collection.json',
manager_count=len(app.managers.managers), manager_count=len(app.managers.managers),
managers=app.managers.managers) managers=app.managers.managers)
@ -319,6 +343,9 @@ def jsonify(obj_type, obj_version, obj):
@app.route('/redfish/v1/Managers/<identity>', methods=['GET']) @app.route('/redfish/v1/Managers/<identity>', methods=['GET'])
@api_utils.returns_json @api_utils.returns_json
def manager_resource(identity): def manager_resource(identity):
if app.feature_set == "minimum":
raise error.FeatureNotAvailable("Managers")
app.logger.debug('Serving resources for manager "%s"', identity) app.logger.debug('Serving resources for manager "%s"', identity)
manager = app.managers.get_manager(identity) manager = app.managers.get_manager(identity)
@ -326,22 +353,11 @@ def manager_resource(identity):
chassis = app.managers.get_managed_chassis(manager) chassis = app.managers.get_managed_chassis(manager)
uuid = manager['UUID'] uuid = manager['UUID']
return jsonify('Manager', 'v1_3_1', { result = {
"Id": manager['Id'], "Id": manager['Id'],
"Name": manager.get('Name'), "Name": manager.get('Name'),
"UUID": uuid, "UUID": uuid,
"ServiceEntryPointUUID": manager.get('ServiceEntryPointUUID'),
"ManagerType": "BMC", "ManagerType": "BMC",
"Description": "Contoso BMC",
"Model": "Joo Janta 200",
"DateTime": datetime.now().strftime('%Y-%M-%dT%H:%M:%S+00:00'),
"DateTimeLocalOffset": "+00:00",
"Status": {
"State": "Enabled",
"Health": "OK"
},
"PowerState": "On",
"FirmwareVersion": "1.00",
"VirtualMedia": { "VirtualMedia": {
"@odata.id": "/redfish/v1/Systems/%s/VirtualMedia" % systems[0], "@odata.id": "/redfish/v1/Systems/%s/VirtualMedia" % systems[0],
}, },
@ -356,11 +372,27 @@ def manager_resource(identity):
{ {
"@odata.id": "/redfish/v1/Chassis/%s" % ch "@odata.id": "/redfish/v1/Chassis/%s" % ch
} }
for ch in chassis for ch in chassis if app.feature_set == "full"
] ]
}, },
"@odata.id": "/redfish/v1/Managers/%s" % uuid "@odata.id": "/redfish/v1/Managers/%s" % uuid,
}) }
if app.feature_set == "full":
result.update({
"ServiceEntryPointUUID": manager.get('ServiceEntryPointUUID'),
"Description": "Contoso BMC",
"Model": "Joo Janta 200",
"DateTime": datetime.now().strftime('%Y-%M-%dT%H:%M:%S+00:00'),
"DateTimeLocalOffset": "+00:00",
"Status": {
"State": "Enabled",
"Health": "OK"
},
"PowerState": "On",
"FirmwareVersion": "1.00",
})
return jsonify('Manager', 'v1_3_1', result)
@app.route('/redfish/v1/Systems') @app.route('/redfish/v1/Systems')
@ -371,7 +403,7 @@ def system_collection_resource():
app.logger.debug('Serving systems list') app.logger.debug('Serving systems list')
return flask.render_template( return app.render_template(
'system_collection.json', system_count=len(systems), systems=systems) 'system_collection.json', system_count=len(systems), systems=systems)
@ -390,7 +422,7 @@ def system_resource(identity):
except error.NotSupportedError: except error.NotSupportedError:
return None return None
return flask.render_template( return app.render_template(
'system.json', 'system.json',
identity=identity, identity=identity,
name=app.systems.name(identity), name=app.systems.name(identity),
@ -414,6 +446,8 @@ def system_resource(identity):
if not boot and not indicator_led_state: if not boot and not indicator_led_state:
return ('PATCH only works for Boot and ' return ('PATCH only works for Boot and '
'IndicatorLED elements'), 400 'IndicatorLED elements'), 400
if indicator_led_state and app.feature_set != "full":
raise error.FeatureNotAvailable("IndicatorLED", code=400)
if boot: if boot:
target = boot.get('BootSourceOverrideTarget') target = boot.get('BootSourceOverrideTarget')
@ -492,9 +526,12 @@ def system_resource(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def ethernet_interfaces_collection(identity): def ethernet_interfaces_collection(identity):
if app.feature_set == "minimum":
raise error.FeatureNotAvailable("EthernetInterfaces")
nics = app.systems.get_nics(identity) nics = app.systems.get_nics(identity)
return flask.render_template( return app.render_template(
'ethernet_interfaces_collection.json', identity=identity, 'ethernet_interfaces_collection.json', identity=identity,
nics=nics) nics=nics)
@ -504,11 +541,14 @@ def ethernet_interfaces_collection(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def ethernet_interface(identity, nic_id): def ethernet_interface(identity, nic_id):
if app.feature_set == "minimum":
raise error.FeatureNotAvailable("EthernetInterfaces")
nics = app.systems.get_nics(identity) nics = app.systems.get_nics(identity)
for nic in nics: for nic in nics:
if nic['id'] == nic_id: if nic['id'] == nic_id:
return flask.render_template( return app.render_template(
'ethernet_interface.json', identity=identity, nic=nic) 'ethernet_interface.json', identity=identity, nic=nic)
raise error.NotFound() raise error.NotFound()
@ -519,9 +559,12 @@ def ethernet_interface(identity, nic_id):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def processors_collection(identity): def processors_collection(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Processors")
processors = app.systems.get_processors(identity) processors = app.systems.get_processors(identity)
return flask.render_template( return app.render_template(
'processors_collection.json', identity=identity, 'processors_collection.json', identity=identity,
processors=processors) processors=processors)
@ -531,11 +574,14 @@ def processors_collection(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def processor(identity, processor_id): def processor(identity, processor_id):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Processors")
processors = app.systems.get_processors(identity) processors = app.systems.get_processors(identity)
for proc in processors: for proc in processors:
if proc['id'] == processor_id: if proc['id'] == processor_id:
return flask.render_template( return app.render_template(
'processor.json', identity=identity, processor=proc) 'processor.json', identity=identity, processor=proc)
raise error.NotFound() raise error.NotFound()
@ -560,11 +606,14 @@ def system_reset_action(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def bios(identity): def bios(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("BIOS")
bios = app.systems.get_bios(identity) bios = app.systems.get_bios(identity)
app.logger.debug('Serving BIOS for system "%s"', identity) app.logger.debug('Serving BIOS for system "%s"', identity)
return flask.render_template( return app.render_template(
'bios.json', 'bios.json',
identity=identity, identity=identity,
bios_current_attributes=json.dumps(bios, sort_keys=True, indent=6)) bios_current_attributes=json.dumps(bios, sort_keys=True, indent=6))
@ -575,13 +624,15 @@ def bios(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def bios_settings(identity): def bios_settings(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("BIOS")
if flask.request.method == 'GET': if flask.request.method == 'GET':
bios = app.systems.get_bios(identity) bios = app.systems.get_bios(identity)
app.logger.debug('Serving BIOS Settings for system "%s"', identity) app.logger.debug('Serving BIOS Settings for system "%s"', identity)
return flask.render_template( return app.render_template(
'bios_settings.json', 'bios_settings.json',
identity=identity, identity=identity,
bios_pending_attributes=json.dumps(bios, sort_keys=True, indent=6)) bios_pending_attributes=json.dumps(bios, sort_keys=True, indent=6))
@ -601,6 +652,9 @@ def bios_settings(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def system_reset_bios(identity): def system_reset_bios(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("BIOS")
app.systems.reset_bios(identity) app.systems.reset_bios(identity)
app.logger.info('BIOS for system "%s" reset', identity) app.logger.info('BIOS for system "%s" reset', identity)
@ -613,13 +667,15 @@ def system_reset_bios(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def secure_boot(identity): def secure_boot(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("SecureBoot")
if flask.request.method == 'GET': if flask.request.method == 'GET':
secure = app.systems.get_secure_boot(identity) secure = app.systems.get_secure_boot(identity)
app.logger.debug('Serving secure boot for system "%s"', identity) app.logger.debug('Serving secure boot for system "%s"', identity)
return flask.render_template( return app.render_template(
'secure_boot.json', 'secure_boot.json',
identity=identity, identity=identity,
secure_boot_enable=secure, secure_boot_enable=secure,
@ -640,10 +696,13 @@ def secure_boot(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def simple_storage_collection(identity): def simple_storage_collection(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("SimpleStorage")
simple_storage_controllers = ( simple_storage_controllers = (
app.systems.get_simple_storage_collection(identity)) app.systems.get_simple_storage_collection(identity))
return flask.render_template( return app.render_template(
'simple_storage_collection.json', identity=identity, 'simple_storage_collection.json', identity=identity,
simple_storage_controllers=simple_storage_controllers) simple_storage_controllers=simple_storage_controllers)
@ -653,6 +712,9 @@ def simple_storage_collection(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def simple_storage(identity, simple_storage_id): def simple_storage(identity, simple_storage_id):
if app.feature_set != "full":
raise error.FeatureNotAvailable("SimpleStorage")
simple_storage_controllers = ( simple_storage_controllers = (
app.systems.get_simple_storage_collection(identity)) app.systems.get_simple_storage_collection(identity))
try: try:
@ -660,8 +722,8 @@ def simple_storage(identity, simple_storage_id):
except KeyError: except KeyError:
app.logger.debug('"%s" Simple Storage resource was not found') app.logger.debug('"%s" Simple Storage resource was not found')
raise error.NotFound() raise error.NotFound()
return flask.render_template('simple_storage.json', identity=identity, return app.render_template('simple_storage.json', identity=identity,
simple_storage=storage_controller) simple_storage=storage_controller)
@app.route('/redfish/v1/Systems/<identity>/Storage', @app.route('/redfish/v1/Systems/<identity>/Storage',
@ -669,11 +731,14 @@ def simple_storage(identity, simple_storage_id):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def storage_collection(identity): def storage_collection(identity):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Storage")
uuid = app.systems.uuid(identity) uuid = app.systems.uuid(identity)
storage_col = app.storage.get_storage_col(uuid) storage_col = app.storage.get_storage_col(uuid)
return flask.render_template( return app.render_template(
'storage_collection.json', identity=identity, 'storage_collection.json', identity=identity,
storage_col=storage_col) storage_col=storage_col)
@ -683,12 +748,15 @@ def storage_collection(identity):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def storage(identity, storage_id): def storage(identity, storage_id):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Storage")
uuid = app.systems.uuid(identity) uuid = app.systems.uuid(identity)
storage_col = app.storage.get_storage_col(uuid) storage_col = app.storage.get_storage_col(uuid)
for stg in storage_col: for stg in storage_col:
if stg['Id'] == storage_id: if stg['Id'] == storage_id:
return flask.render_template( return app.render_template(
'storage.json', identity=identity, storage=stg) 'storage.json', identity=identity, storage=stg)
raise error.NotFound() raise error.NotFound()
@ -699,12 +767,15 @@ def storage(identity, storage_id):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def drive_resource(identity, stg_id, drv_id): def drive_resource(identity, stg_id, drv_id):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Storage")
uuid = app.systems.uuid(identity) uuid = app.systems.uuid(identity)
drives = app.drives.get_drives(uuid, stg_id) drives = app.drives.get_drives(uuid, stg_id)
for drv in drives: for drv in drives:
if drv['Id'] == drv_id: if drv['Id'] == drv_id:
return flask.render_template( return app.render_template(
'drive.json', identity=identity, storage_id=stg_id, drive=drv) 'drive.json', identity=identity, storage_id=stg_id, drive=drv)
raise error.NotFound() raise error.NotFound()
@ -715,6 +786,9 @@ def drive_resource(identity, stg_id, drv_id):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def volumes_collection(identity, storage_id): def volumes_collection(identity, storage_id):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Storage")
uuid = app.systems.uuid(identity) uuid = app.systems.uuid(identity)
if flask.request.method == 'GET': if flask.request.method == 'GET':
@ -729,7 +803,7 @@ def volumes_collection(identity, storage_id):
else: else:
vol_ids.append(vol_id) vol_ids.append(vol_id)
return flask.render_template( return app.render_template(
'volume_collection.json', identity=identity, 'volume_collection.json', identity=identity,
storage_id=storage_id, volume_col=vol_ids) storage_id=storage_id, volume_col=vol_ids)
@ -757,6 +831,9 @@ def volumes_collection(identity, storage_id):
@api_utils.ensure_instance_access @api_utils.ensure_instance_access
@api_utils.returns_json @api_utils.returns_json
def volume(identity, stg_id, vol_id): def volume(identity, stg_id, vol_id):
if app.feature_set != "full":
raise error.FeatureNotAvailable("Storage")
uuid = app.systems.uuid(identity) uuid = app.systems.uuid(identity)
vol_col = app.volumes.get_volumes_col(uuid, stg_id) vol_col = app.volumes.get_volumes_col(uuid, stg_id)
@ -766,7 +843,7 @@ def volume(identity, stg_id, vol_id):
if not vol_id: if not vol_id:
app.volumes.delete_volume(uuid, stg_id, vol) app.volumes.delete_volume(uuid, stg_id, vol)
else: else:
return flask.render_template( return app.render_template(
'volume.json', identity=identity, storage_id=stg_id, 'volume.json', identity=identity, storage_id=stg_id,
volume=vol) volume=vol)
@ -776,44 +853,59 @@ def volume(identity, stg_id, vol_id):
@app.route('/redfish/v1/Registries') @app.route('/redfish/v1/Registries')
@api_utils.returns_json @api_utils.returns_json
def registry_file_collection(): def registry_file_collection():
if app.feature_set != "full":
raise error.FeatureNotAvailable("Registries")
app.logger.debug('Serving registry file collection') app.logger.debug('Serving registry file collection')
return flask.render_template( return app.render_template(
'registry_file_collection.json') 'registry_file_collection.json')
@app.route('/redfish/v1/Registries/BiosAttributeRegistry.v1_0_0') @app.route('/redfish/v1/Registries/BiosAttributeRegistry.v1_0_0')
@api_utils.returns_json @api_utils.returns_json
def bios_attribute_registry_file(): def bios_attribute_registry_file():
if app.feature_set != "full":
raise error.FeatureNotAvailable("Registries")
app.logger.debug('Serving BIOS attribute registry file') app.logger.debug('Serving BIOS attribute registry file')
return flask.render_template( return app.render_template(
'bios_attribute_registry_file.json') 'bios_attribute_registry_file.json')
@app.route('/redfish/v1/Registries/Messages') @app.route('/redfish/v1/Registries/Messages')
@api_utils.returns_json @api_utils.returns_json
def message_registry_file(): def message_registry_file():
if app.feature_set != "full":
raise error.FeatureNotAvailable("Registries")
app.logger.debug('Serving message registry file') app.logger.debug('Serving message registry file')
return flask.render_template( return app.render_template(
'message_registry_file.json') 'message_registry_file.json')
@app.route('/redfish/v1/Systems/Bios/BiosRegistry') @app.route('/redfish/v1/Systems/Bios/BiosRegistry')
@api_utils.returns_json @api_utils.returns_json
def bios_registry(): def bios_registry():
if app.feature_set != "full":
raise error.FeatureNotAvailable("Registries")
app.logger.debug('Serving BIOS registry') app.logger.debug('Serving BIOS registry')
return flask.render_template('bios_registry.json') return app.render_template('bios_registry.json')
@app.route('/redfish/v1/Registries/Messages/Registry') @app.route('/redfish/v1/Registries/Messages/Registry')
@api_utils.returns_json @api_utils.returns_json
def message_registry(): def message_registry():
if app.feature_set != "full":
raise error.FeatureNotAvailable("Registries")
app.logger.debug('Serving message registry') app.logger.debug('Serving message registry')
return flask.render_template('message_registry.json') return app.render_template('message_registry.json')
def parse_args(): def parse_args():
@ -843,6 +935,10 @@ def parse_args():
type=str, type=str,
help='SSL key to use for HTTPS. Can also be set' help='SSL key to use for HTTPS. Can also be set'
'via config variable SUSHY_EMULATOR_SSL_KEY.') 'via config variable SUSHY_EMULATOR_SSL_KEY.')
parser.add_argument('--feature-set',
type=str, choices=['full', 'vmedia', 'minimum'],
help='Feature set to provide. Can also be set'
'via config variable SUSHY_EMULATOR_FEATURE_SET.')
backend_group = parser.add_mutually_exclusive_group() backend_group = parser.add_mutually_exclusive_group()
backend_group.add_argument('--os-cloud', backend_group.add_argument('--os-cloud',
type=str, type=str,
@ -908,6 +1004,9 @@ def main():
if args.ssl_key: if args.ssl_key:
app.config['SUSHY_EMULATOR_SSL_KEY'] = args.ssl_key app.config['SUSHY_EMULATOR_SSL_KEY'] = args.ssl_key
if args.feature_set:
app.config['SUSHY_EMULATOR_FEATURE_SET'] = args.feature_set
ssl_context = None ssl_context = None
ssl_certificate = app.config.get('SUSHY_EMULATOR_SSL_CERT') ssl_certificate = app.config.get('SUSHY_EMULATOR_SSL_CERT')
ssl_key = app.config.get('SUSHY_EMULATOR_SSL_KEY') ssl_key = app.config.get('SUSHY_EMULATOR_SSL_KEY')

View File

@ -4,21 +4,27 @@
"Name": "Redvirt Service", "Name": "Redvirt Service",
"RedfishVersion": "1.5.0", "RedfishVersion": "1.5.0",
"UUID": "85775665-c110-4b85-8989-e6162170b3ec", "UUID": "85775665-c110-4b85-8989-e6162170b3ec",
{% if feature_set == "full" %}
"Chassis": { "Chassis": {
"@odata.id": "/redfish/v1/Chassis" "@odata.id": "/redfish/v1/Chassis"
}, },
{% endif %}
"Systems": { "Systems": {
"@odata.id": "/redfish/v1/Systems" "@odata.id": "/redfish/v1/Systems"
}, },
{% if feature_set != "minimum" %}
"Managers": { "Managers": {
"@odata.id": "/redfish/v1/Managers" "@odata.id": "/redfish/v1/Managers"
}, },
{% endif %}
{% if feature_set == "full" %}
"Registries": { "Registries": {
"@odata.id": "/redfish/v1/Registries" "@odata.id": "/redfish/v1/Registries"
}, },
"CertificateService": { "CertificateService": {
"@odata.id": "/redfish/v1/CertificateService" "@odata.id": "/redfish/v1/CertificateService"
}, },
{% endif %}
"@odata.id": "/redfish/v1/", "@odata.id": "/redfish/v1/",
"@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." "@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
} }

View File

@ -3,12 +3,14 @@
"Id": {{ identity|string|tojson }}, "Id": {{ identity|string|tojson }},
"Name": {{ name|string|tojson }}, "Name": {{ name|string|tojson }},
"UUID": {{ uuid|string|tojson }}, "UUID": {{ uuid|string|tojson }},
{%- if feature_set == "full" %}
"Manufacturer": "Sushy Emulator", "Manufacturer": "Sushy Emulator",
"Status": { "Status": {
"State": "Enabled", "State": "Enabled",
"Health": "OK", "Health": "OK",
"HealthRollUp": "OK" "HealthRollUp": "OK"
}, },
{% endif %}
{%- if power_state %} {%- if power_state %}
"PowerState": {{ power_state|string|tojson }}, "PowerState": {{ power_state|string|tojson }},
{%- endif %} {%- endif %}
@ -40,6 +42,7 @@
"BootSourceOverrideEnabled": "Continuous" "BootSourceOverrideEnabled": "Continuous"
{%- endif %} {%- endif %}
}, },
{%- if feature_set == "full" %}
"ProcessorSummary": { "ProcessorSummary": {
{%- if total_cpus %} {%- if total_cpus %}
"Count": {{ total_cpus }}, "Count": {{ total_cpus }},
@ -70,9 +73,6 @@
"Memory": { "Memory": {
"@odata.id": {{ "/redfish/v1/Systems/%s/Memory"|format(identity)|tojson }} "@odata.id": {{ "/redfish/v1/Systems/%s/Memory"|format(identity)|tojson }}
}, },
"EthernetInterfaces": {
"@odata.id": {{ "/redfish/v1/Systems/%s/EthernetInterfaces"|format(identity)|tojson }}
},
"SecureBoot": { "SecureBoot": {
"@odata.id": {{ "/redfish/v1/Systems/%s/SecureBoot"|format(identity)|tojson }} "@odata.id": {{ "/redfish/v1/Systems/%s/SecureBoot"|format(identity)|tojson }}
}, },
@ -85,10 +85,17 @@
{%- if indicator_led %} {%- if indicator_led %}
"IndicatorLED": {{ indicator_led|string|tojson }}, "IndicatorLED": {{ indicator_led|string|tojson }},
{%- endif %} {%- endif %}
{%- endif %}
{%- if feature_set != "minimum" %}
"EthernetInterfaces": {
"@odata.id": {{ "/redfish/v1/Systems/%s/EthernetInterfaces"|format(identity)|tojson }}
},
"VirtualMedia": { "VirtualMedia": {
"@odata.id": {{ "/redfish/v1/Systems/%s/VirtualMedia"|format(identity)|tojson }} "@odata.id": {{ "/redfish/v1/Systems/%s/VirtualMedia"|format(identity)|tojson }}
}, },
{%- endif %}
"Links": { "Links": {
{%- if feature_set == "full" %}
"Chassis": [ "Chassis": [
{%- for chassis_ in chassis %} {%- for chassis_ in chassis %}
{ {
@ -96,6 +103,8 @@
}{% if not loop.last %},{% endif %} }{% if not loop.last %},{% endif %}
{% endfor -%} {% endfor -%}
], ],
{% endif %}
{%- if feature_set != "minimum" %}
"ManagedBy": [ "ManagedBy": [
{%- for manager in managers %} {%- for manager in managers %}
{ {
@ -103,6 +112,7 @@
}{% if not loop.last %},{% endif %} }{% if not loop.last %},{% endif %}
{% endfor -%} {% endfor -%}
] ]
{% endif %}
}, },
"Actions": { "Actions": {
"#ComputerSystem.Reset": { "#ComputerSystem.Reset": {

View File

@ -45,3 +45,10 @@ class BadRequest(FishyError):
def __init__(self, msg, code=400): def __init__(self, msg, code=400):
super().__init__(msg, code) super().__init__(msg, code)
class FeatureNotAvailable(NotFound):
"""Feature is not available."""
def __init__(self, feature, code=404):
super().__init__(f"Feature {feature} not available", code=code)

View File

@ -36,6 +36,12 @@ class EmulatorTestCase(base.BaseTestCase):
super(EmulatorTestCase, self).setUp() super(EmulatorTestCase, self).setUp()
def set_feature_set(self, new_feature_set):
main.app.config['SUSHY_EMULATOR_FEATURE_SET'] = new_feature_set
self.addCleanup(
lambda: main.app.config.pop('SUSHY_EMULATOR_FEATURE_SET', None)
)
class CommonTestCase(EmulatorTestCase): class CommonTestCase(EmulatorTestCase):
@ -52,6 +58,22 @@ class CommonTestCase(EmulatorTestCase):
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
self.assertEqual('RedvirtService', response.json['Id']) self.assertEqual('RedvirtService', response.json['Id'])
def test_root_resource_only_vmedia(self):
self.set_feature_set("vmedia")
response = self.app.get('/redfish/v1/')
self.assertEqual(200, response.status_code)
self.assertEqual(
{'Id', 'Name', 'RedfishVersion', 'UUID', 'Systems', 'Managers'},
{x for x in response.json if not x.startswith('@')})
def test_root_resource_minimum(self):
self.set_feature_set("minimum")
response = self.app.get('/redfish/v1/')
self.assertEqual(200, response.status_code)
self.assertEqual(
{'Id', 'Name', 'RedfishVersion', 'UUID', 'Systems'},
{x for x in response.json if not x.startswith('@')})
TEST_PASSWD = \ TEST_PASSWD = \
b"admin:$2y$05$mYl8KMwM94l4LR/sw1teIeA6P2u8gfX16e8wvT7NmGgAM5r9jgLl." b"admin:$2y$05$mYl8KMwM94l4LR/sw1teIeA6P2u8gfX16e8wvT7NmGgAM5r9jgLl."
@ -210,6 +232,33 @@ class ManagersTestCase(EmulatorTestCase):
response.json['Links']['ManagerForServers']) response.json['Links']['ManagerForServers'])
self.assertEqual([{'@odata.id': '/redfish/v1/Chassis/chassis0'}], self.assertEqual([{'@odata.id': '/redfish/v1/Chassis/chassis0'}],
response.json['Links']['ManagerForChassis']) response.json['Links']['ManagerForChassis'])
self.assertEqual({'@odata.id': '/redfish/v1/Systems/xxx/VirtualMedia'},
response.json['VirtualMedia'])
@patch_resource('managers')
def test_manager_resource_get_reduced_feature_set(self, managers_mock):
self.set_feature_set("vmedia")
managers_mock = managers_mock.return_value
managers_mock.managers = ['xxxx-yyyy-zzzz']
managers_mock.get_manager.return_value = {
'UUID': 'xxxx-yyyy-zzzz',
'Name': 'name',
'Id': 'xxxx-yyyy-zzzz',
}
managers_mock.get_managed_systems.return_value = ['xxx']
managers_mock.get_managed_chassis.return_value = ['chassis0']
response = self.app.get('/redfish/v1/Managers/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code, response.json)
self.assertEqual('xxxx-yyyy-zzzz', response.json['Id'])
self.assertEqual('xxxx-yyyy-zzzz', response.json['UUID'])
self.assertNotIn('ServiceEntryPointUUID', response.json)
self.assertEqual([{'@odata.id': '/redfish/v1/Systems/xxx'}],
response.json['Links']['ManagerForServers'])
self.assertNotIn('Chassis', response.json['Links'])
self.assertEqual({'@odata.id': '/redfish/v1/Systems/xxx/VirtualMedia'},
response.json['VirtualMedia'])
class SystemsTestCase(EmulatorTestCase): class SystemsTestCase(EmulatorTestCase):
@ -263,6 +312,81 @@ class SystemsTestCase(EmulatorTestCase):
self.assertEqual( self.assertEqual(
[{'@odata.id': '/redfish/v1/Chassis/chassis0'}], [{'@odata.id': '/redfish/v1/Chassis/chassis0'}],
response.json['Links']['Chassis']) response.json['Links']['Chassis'])
self.assertEqual(
{'@odata.id': '/redfish/v1/Systems/xxxx-yyyy-zzzz/VirtualMedia'},
response.json['VirtualMedia'])
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_system_resource_get_reduced_feature_set(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
self.set_feature_set("vmedia")
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.return_value = 'Legacy'
managers_mock.return_value.get_managers_for_system.return_value = [
'aaaa-bbbb-cccc']
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertEqual('xxxx-yyyy-zzzz', response.json['Id'])
self.assertEqual('zzzz-yyyy-xxxx', response.json['UUID'])
self.assertEqual('On', response.json['PowerState'])
self.assertNotIn('IndicatorLED', response.json)
self.assertNotIn('MemorySummary', response.json)
self.assertNotIn('ProcessorSummary', response.json)
self.assertNotIn('BiosVersion', response.json)
self.assertNotIn('Bios', response.json)
self.assertEqual(
'Cd', response.json['Boot']['BootSourceOverrideTarget'])
self.assertEqual(
'Legacy', response.json['Boot']['BootSourceOverrideMode'])
self.assertEqual(
[{'@odata.id': '/redfish/v1/Managers/aaaa-bbbb-cccc'}],
response.json['Links']['ManagedBy'])
self.assertNotIn('Chassis', response.json['Links'])
self.assertEqual(
{'@odata.id': '/redfish/v1/Systems/xxxx-yyyy-zzzz/VirtualMedia'},
response.json['VirtualMedia'])
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_system_resource_get_minimum_feature_set(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
self.set_feature_set("minimum")
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.return_value = 'Legacy'
managers_mock.return_value.get_managers_for_system.return_value = [
'aaaa-bbbb-cccc']
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertEqual('xxxx-yyyy-zzzz', response.json['Id'])
self.assertEqual('zzzz-yyyy-xxxx', response.json['UUID'])
self.assertEqual('On', response.json['PowerState'])
self.assertNotIn('IndicatorLED', response.json)
self.assertNotIn('MemorySummary', response.json)
self.assertNotIn('ProcessorSummary', response.json)
self.assertNotIn('BiosVersion', response.json)
self.assertNotIn('Bios', response.json)
self.assertEqual(
'Cd', response.json['Boot']['BootSourceOverrideTarget'])
self.assertEqual(
'Legacy', response.json['Boot']['BootSourceOverrideMode'])
self.assertNotIn('ManagedBy', response.json['Links'])
self.assertNotIn('Chassis', response.json['Links'])
self.assertNotIn('VirtualMedia', response.json)
@patch_resource('systems') @patch_resource('systems')
def test_system_resource_patch(self, systems_mock): def test_system_resource_patch(self, systems_mock):
@ -336,6 +460,18 @@ class SystemsTestCase(EmulatorTestCase):
json=data) json=data)
self.assertEqual(500, response.status_code) self.assertEqual(500, response.status_code)
@patch_resource('indicators')
@patch_resource('systems')
def test_system_indicator_reduced_feature_set(self, systems_mock,
indicators_mock):
self.set_feature_set("vmedia")
systems_mock.return_value.uuid.return_value = self.uuid
data = {'IndicatorLED': 'Off'}
response = self.app.patch('/redfish/v1/Systems/xxxx-yyyy-zzzz',
json=data)
self.assertEqual(400, response.status_code)
@patch_resource('systems') @patch_resource('systems')
class BiosTestCase(EmulatorTestCase): class BiosTestCase(EmulatorTestCase):