[WIP] Support uploading certificates for virtual media

Change-Id: I30a5c673ab91486cafbe1383c5fe58142102a7c2
This commit is contained in:
Dmitry Tantsur 2021-08-26 19:18:44 +02:00
parent c6a42b999a
commit 0f79b690b4
5 changed files with 173 additions and 6 deletions

View File

@ -18,6 +18,7 @@ from datetime import datetime
import functools
import json
import os
import re
import ssl
import sys
@ -455,11 +456,11 @@ def virtual_media_patch(identity, device):
def virtual_media_certificates(identity, device):
location = \
f'/redfish/v1/Managers/{identity}/VirtualMedia/{device}/Certificates'
certificates = app.vmedia.list_certificates(identity, device)
return flask.render_template(
'certificate_collection.json',
location=location,
# TODO(dtantsur): implement
certificates=[],
certificates=[cert.id for cert in certificates],
)
@ -471,8 +472,56 @@ def virtual_media_add_certificate(identity, device):
if not flask.request.json:
raise error.BadRequest("Empty or malformed certificate")
# TODO(dtantsur): implement
raise error.NotSupportedError("Not implemented")
try:
cert_string = flask.request.json['CertificateString']
cert_type = flask.request.json['CertificateType']
except KeyError as exc:
raise error.BadRequest(f"Missing required parameter {exc}")
app.logger.debug('Adding certificate for virtual media %s at '
'manager "%s"', device, identity)
app.vmedia.add_certificate(identity, device, cert_string, cert_type)
# TODO(dtantsur): should we return a body?
return '', 204
@app.route(
'/redfish/v1/Managers/<identity>/VirtualMedia/<device>'
'/Certificates/<cert_id>',
methods=['GET'])
@returns_json
def virtual_media_get_certificate(identity, device, cert_id):
location = (
f'/redfish/v1/Managers/{identity}/VirtualMedia/{device}/Certificates/'
+ cert_id
)
certificates = app.vmedia.list_certificates(identity, device)
try:
cert = next(c for c in certificates if c.id == cert_id)
except StopIteration:
raise error.NotFound()
return flask.render_template(
'certificate_collection.json',
location=location,
cert_id=cert.id,
cert_string=cert.string,
cert_type=cert.type_,
)
@app.route(
'/redfish/v1/Managers/<identity>/VirtualMedia/<device>'
'/Certificates/<cert_id>',
methods=['DELETE'])
@returns_json
def virtual_media_delete_certificate(identity, device, cert_id):
app.logger.debug('Removing certificate %s for virtual media %s at '
'manager "%s"', cert_id, device, identity)
app.vmedia.delete_certificate(identity, device, cert_id)
return '', 204
@app.route('/redfish/v1/Managers/<identity>/VirtualMedia/<device>'
@ -886,6 +935,49 @@ def volume(identity, stg_id, vol_id):
raise error.NotFound()
@app.route('/redfish/v1/CertificateService', methods=['GET'])
@returns_json
def certificate_service_resource():
app.logger.debug('Serving certificate service')
return flask.render_template(
'certificate_service.json')
_VMEDIA_URI_PATTERN = re.compile(
'/redfish/v1/Managers/([^/]+)/VirtualMedia/([^/]+)/Certificates/([^/]+)/?$'
)
@app.route('/redfish/v1/CertificateService/Actions'
'/CertificateService.ReplaceCertificate',
methods=['POST'])
@ensure_instance_access
@returns_json
def certificate_service_replace_certificate(identity):
if not flask.request.json:
raise error.BadRequest("Empty or malformed certificate")
try:
cert_string = flask.request.json['CertificateString']
cert_type = flask.request.json['CertificateType']
cert_uri = flask.request.json['CertificateUri']
except KeyError as exc:
raise error.BadRequest(f"Missing required parameter {exc}")
match = _VMEDIA_URI_PATTERN.search(cert_uri)
if not match:
raise error.NotFound(
f"Certificates at URI {cert_uri} are not supported")
manager_id, device, cert_id = match.groups()
app.vmedia.replace_certificate(manager_id, device,
cert_id, cert_string, cert_type)
return '', 204
@app.route('/redfish/v1/Registries')
@returns_json
def registry_file_collection():

View File

@ -30,6 +30,11 @@ DeviceInfo = collections.namedtuple(
'DeviceInfo',
['image_name', 'image_url', 'inserted', 'write_protected',
'username', 'password', 'verify'])
Certificate = collections.namedtuple(
'Certificate',
['id', 'string', 'type_'])
_CERT_ID = "Default"
class StaticDriver(base.DriverBase):
@ -150,6 +155,49 @@ class StaticDriver(base.DriverBase):
device_info['Verify'] = verify
self._devices[(identity, device)] = device_info
def add_certificate(self, identity, device, cert_string, cert_type):
device_info = self._get_device(identity, device)
if cert_type != 'PEM':
raise error.BadRequest(
f"Only PEM certificates are supported, got {cert_type}")
if "Certificate" in device_info:
raise error.FishyError(f"Certificate {_CERT_ID} already exists",
code=409)
device_info["Certificate"] = cert_string
self._devices[(identity, device)] = device_info
def replace_certificate(self, identity, device, cert_id,
cert_string, cert_type):
device_info = self._get_device(identity, device)
if cert_id != _CERT_ID or "Certificate" not in device_info:
raise error.NotFound(f"Certificate {cert_id} not found")
if cert_type != 'PEM':
raise error.BadRequest(
f"Only PEM certificates are supported, got {cert_type}")
device_info["Certificate"] = cert_string
self._devices[(identity, device)] = device_info
def list_certificates(self, identity, device):
device_info = self._get_device(identity, device)
try:
certificate = device_info["Certificate"]
except KeyError:
return []
return [Certificate(_CERT_ID, certificate, 'PEM')]
def delete_certificate(self, identity, device, cert_id):
device_info = self._get_device(identity, device)
if cert_id != _CERT_ID or "Certificate" not in device_info:
raise error.NotFound(f"Certificate {cert_id} not found")
del device_info["Certificate"]
self._devices[(identity, device)] = device_info
def _write_from_response(self, image_url, rsp, tmp_file):
with open(tmp_file.name, 'wb') as fl:
for chunk in rsp.iter_content(chunk_size=8192):

View File

@ -0,0 +1,10 @@
{
"@odata.type": "#Certificate.v1_3_0.Certificate",
"Id": {{ cert_id|string|json }},
"Name": "HTTPS Certificate {{ cert_id }}",
"CertificateString": {{ cert_string|string|json }},
"CertificateType": {{ cert_type|string|json }},
"Oem": {},
"@odata.id": {{ location|string|json }},
"@Redfish.Copyright": "Copyright 2014-2021 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -0,0 +1,14 @@
{
"@odata.type": "#CertificateService.v1_0_4.CertificateService",
"Id": "CertificateService",
"Name": "Certificate Service",
"Actions": {
"#CertificateService.ReplaceCertificate": {
"target": "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate",
"@Redfish.ActionInfo": "/redfish/v1/CertificateService/ReplaceCertificateActionInfo"
}
},
"Oem": {},
"@odata.id": "/redfish/v1/CertificateService",
"@Redfish.Copyright": "Copyright 2014-2021 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -1,8 +1,8 @@
{
"@odata.type": "#ServiceRoot.v1_0_2.ServiceRoot",
"@odata.type": "#ServiceRoot.v1_5_0.ServiceRoot",
"Id": "RedvirtService",
"Name": "Redvirt Service",
"RedfishVersion": "1.0.2",
"RedfishVersion": "1.5.0",
"UUID": "85775665-c110-4b85-8989-e6162170b3ec",
"Systems": {
"@odata.id": "/redfish/v1/Systems"
@ -13,6 +13,9 @@
"Registries": {
"@odata.id": "/redfish/v1/Registries"
},
"CertificateService": {
"@odata.id": "/redfish/v1/CertificateService"
},
"@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."
}