317 lines
12 KiB
Python
317 lines
12 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from urllib import parse as urlparse
|
|
|
|
import jsonschema
|
|
from oslo_log import log
|
|
from oslo_utils import fileutils
|
|
|
|
from ironic.common import exception
|
|
from ironic.common.i18n import _
|
|
from ironic.common import image_service
|
|
from ironic.common import swift
|
|
from ironic.conf import CONF
|
|
from ironic.drivers.modules.redfish import utils as redfish_utils
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
_UPDATE_FIRMWARE_SCHEMA = {
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"title": "update_firmware clean step schema",
|
|
"type": "array",
|
|
# list of firmware update images
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["url", "checksum"],
|
|
"properties": {
|
|
"url": {
|
|
"description": "URL for firmware file",
|
|
"type": "string",
|
|
"minLength": 1
|
|
},
|
|
"checksum": {
|
|
"description": "SHA1 checksum for firmware file",
|
|
"type": "string",
|
|
"minLength": 1
|
|
},
|
|
"wait": {
|
|
"description": "optional wait time for firmware update",
|
|
"type": "integer",
|
|
"minimum": 1
|
|
},
|
|
"source":
|
|
{
|
|
"description": "optional firmware_source to override global "
|
|
"setting for firmware file",
|
|
"type": "string",
|
|
"enum": ["http", "local", "swift"]
|
|
}
|
|
},
|
|
"additionalProperties": False
|
|
}
|
|
}
|
|
|
|
_FIRMWARE_INTERFACE_UPDATE_SCHEMA = {
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"title": "update_firmware clean step schema",
|
|
"type": "array",
|
|
"minItems": 1,
|
|
# list of firmware update images
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["component", "url"],
|
|
"properties": {
|
|
"component": {
|
|
"description": "name of the firmware component to be updated",
|
|
"type": "string",
|
|
"enum": redfish_utils.FIRMWARE_COMPONENTS
|
|
},
|
|
"url": {
|
|
"description": "URL for firmware file",
|
|
"type": "string",
|
|
"minLength": 1
|
|
},
|
|
"wait": {
|
|
"description": "optional wait time for firmware update",
|
|
"type": "integer",
|
|
"minimum": 1
|
|
}
|
|
},
|
|
"additionalProperties": False
|
|
}
|
|
}
|
|
|
|
_FIRMWARE_SUBDIR = 'firmware'
|
|
|
|
|
|
def validate_update_firmware_args(firmware_images):
|
|
"""Validate ``update_firmware`` step input argument
|
|
|
|
:param firmware_images: args to validate.
|
|
:raises: InvalidParameterValue When argument is not valid
|
|
"""
|
|
try:
|
|
jsonschema.validate(firmware_images, _UPDATE_FIRMWARE_SCHEMA)
|
|
except jsonschema.ValidationError as err:
|
|
raise exception.InvalidParameterValue(
|
|
_('Invalid firmware update %(firmware_images)s. Errors: %(err)s')
|
|
% {'firmware_images': firmware_images, 'err': err})
|
|
|
|
|
|
def validate_firmware_interface_update_args(settings):
|
|
"""Validate ``update`` step input argument
|
|
|
|
:param settings: args to validate.
|
|
:raises: InvalidParameterValue When argument is not valid
|
|
"""
|
|
try:
|
|
jsonschema.validate(settings, _FIRMWARE_INTERFACE_UPDATE_SCHEMA)
|
|
except jsonschema.ValidationError as err:
|
|
raise exception.InvalidParameterValue(
|
|
_('Invalid firmware update %(settings)s. Errors: %(err)s')
|
|
% {'settings': settings, 'err': err})
|
|
|
|
|
|
def get_swift_temp_url(parsed_url):
|
|
"""Gets Swift temporary URL
|
|
|
|
:param parsed_url: Parsed URL from URL in format
|
|
swift://container/[sub-folder/]file
|
|
:returns: Swift temporary URL
|
|
"""
|
|
return swift.SwiftAPI().get_temp_url(
|
|
parsed_url.netloc, parsed_url.path.lstrip('/'),
|
|
CONF.redfish.swift_object_expiry_timeout)
|
|
|
|
|
|
def download_to_temp(node, url):
|
|
"""Downloads to temporary location from given URL
|
|
|
|
:param node: Node for which to download to temporary location
|
|
:param url: URL to download from
|
|
:returns: File path of temporary location file is downloaded to
|
|
"""
|
|
parsed_url = urlparse.urlparse(url)
|
|
scheme = parsed_url.scheme.lower()
|
|
if scheme not in ('http', 'swift', 'file'):
|
|
raise exception.InvalidParameterValue(
|
|
_('%(scheme)s is not supported for %(url)s.')
|
|
% {'scheme': scheme, 'url': parsed_url.geturl()})
|
|
|
|
tempdir = os.path.join(tempfile.gettempdir(), node.uuid)
|
|
os.makedirs(tempdir, exist_ok=True)
|
|
temp_file = os.path.join(
|
|
tempdir,
|
|
os.path.basename(parsed_url.path))
|
|
LOG.debug('For node %(node)s firmware at %(url)s will be downloaded to '
|
|
'temporary location at %(temp_file)s',
|
|
{'node': node.uuid, 'url': url, 'temp_file': temp_file})
|
|
if scheme == 'http':
|
|
with open(temp_file, 'wb') as tf:
|
|
image_service.HttpImageService().download(url, tf)
|
|
elif scheme == 'swift':
|
|
swift_url = get_swift_temp_url(parsed_url)
|
|
with open(temp_file, 'wb') as tf:
|
|
image_service.HttpImageService().download(swift_url, tf)
|
|
elif scheme == 'file':
|
|
with open(temp_file, 'wb') as tf:
|
|
image_service.FileImageService().download(
|
|
parsed_url.path, tf)
|
|
|
|
return temp_file
|
|
|
|
|
|
def verify_checksum(node, checksum, file_path):
|
|
"""Verify checksum.
|
|
|
|
:param node: Node for which file to verify checksum
|
|
:param checksum: Expected checksum value
|
|
:param file_path: File path for which to verify checksum
|
|
:raises RedfishError: When checksum does not match
|
|
"""
|
|
if len(checksum) <= 41:
|
|
# SHA1: 40 bytes long
|
|
calculated_checksum = fileutils.compute_file_checksum(
|
|
file_path, algorithm='sha1')
|
|
elif len(checksum) <= 64:
|
|
calculated_checksum = fileutils.compute_file_checksum(
|
|
file_path, algorithm='sha256')
|
|
elif len(checksum) <= 128:
|
|
calculated_checksum = fileutils.compute_file_checksum(
|
|
file_path, algorithm='sha512')
|
|
else:
|
|
raise exception.RedfishError(
|
|
_('Unable to identify checksum to perform firmware file checksum '
|
|
'calculation. Please validate your input in and try again. '
|
|
'Received: %(checksum)s')
|
|
% {'checksum': checksum})
|
|
if checksum != calculated_checksum:
|
|
raise exception.RedfishError(
|
|
_('For node %(node)s firmware file %(temp_file)s checksums do not '
|
|
'match. Expected: %(checksum)s, calculated: '
|
|
'%(calculated_checksum)s.')
|
|
% {'node': node.uuid, 'temp_file': file_path, 'checksum': checksum,
|
|
'calculated_checksum': calculated_checksum})
|
|
|
|
|
|
def stage(node, source, temp_file):
|
|
"""Stage temporary file to configured location
|
|
|
|
:param node: Node for which to stage the file
|
|
:param source: Where to stage the file. Corresponds to
|
|
CONF.redfish.firmware_source.
|
|
:param temp_file: File path of temporary file to stage
|
|
:returns: Tuple of staged URL and source (http or swift) that needs
|
|
cleanup of staged files afterwards.
|
|
:raises RedfishError: If staging to HTTP server has failed.
|
|
"""
|
|
staged_url = None
|
|
filename = os.path.basename(temp_file)
|
|
if source in ('http', 'local'):
|
|
http_url = CONF.deploy.external_http_url or CONF.deploy.http_url
|
|
staged_url = urlparse.urljoin(
|
|
http_url, "/".join([_FIRMWARE_SUBDIR, node.uuid, filename]))
|
|
staged_folder = os.path.join(
|
|
CONF.deploy.http_root, _FIRMWARE_SUBDIR, node.uuid)
|
|
staged_path = os.path.join(staged_folder, filename)
|
|
LOG.debug('For node %(node)s temporary file %(temp_file)s will be '
|
|
'hard-linked or copied to %(staged_path)s and served over '
|
|
'%(staged_url)s',
|
|
{'node': node.uuid, 'temp_file': temp_file,
|
|
'staged_path': staged_path, 'staged_url': staged_url})
|
|
os.makedirs(staged_folder, exist_ok=True)
|
|
try:
|
|
os.link(temp_file, staged_path)
|
|
os.chmod(temp_file, CONF.redfish.file_permission)
|
|
except OSError as oserror:
|
|
LOG.debug("Could not hardlink file %(temp_file)s to location "
|
|
"%(staged_path)s. Will try to copy it. Error: %(error)s",
|
|
{'temp_file': temp_file, 'staged_path': staged_path,
|
|
'error': oserror})
|
|
try:
|
|
shutil.copyfile(temp_file, staged_path)
|
|
os.chmod(staged_path, CONF.redfish.file_permission)
|
|
except IOError as ioerror:
|
|
raise exception.RedfishError(
|
|
_('For %(node)s failed to copy firmware file '
|
|
'%(temp_file)s to HTTP server root. Error %(error)s')
|
|
% {'node': node.uuid, 'temp_file': temp_file,
|
|
'error': ioerror})
|
|
|
|
elif source == 'swift':
|
|
container = CONF.redfish.swift_container
|
|
timeout = CONF.redfish.swift_object_expiry_timeout
|
|
swift_api = swift.SwiftAPI()
|
|
object_name = "/".join([node.uuid, filename])
|
|
swift_api.create_object(
|
|
container,
|
|
object_name,
|
|
temp_file,
|
|
object_headers={'X-Delete-After': str(timeout)})
|
|
staged_url = swift_api.get_temp_url(
|
|
container, object_name, timeout)
|
|
LOG.debug('For node %(node)s temporary file at %(temp_file)s will be '
|
|
'served from Swift temporary URL %(staged_url)s',
|
|
{'node': node.uuid, 'temp_file': temp_file,
|
|
'staged_url': staged_url})
|
|
|
|
need_cleanup = 'swift' if source == 'swift' else 'http'
|
|
return staged_url, need_cleanup
|
|
|
|
|
|
def cleanup(node):
|
|
"""Clean up staged files
|
|
|
|
:param node: Node for which to clean up. Should contain
|
|
'firmware_cleanup' entry in `driver_internal_info` to indicate
|
|
source(s) to be cleaned up.
|
|
"""
|
|
# Cleaning up temporary just in case there is something when staging
|
|
# to http or swift has failed.
|
|
temp_dir = os.path.join(tempfile.gettempdir(), node.uuid)
|
|
LOG.debug('For node %(node)s cleaning up temporary files, if any, from '
|
|
'%(temp_dir)s.', {'node': node.uuid, 'temp_dir': temp_dir})
|
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
|
|
cleanup = node.driver_internal_info.get('firmware_cleanup')
|
|
if not cleanup:
|
|
return
|
|
|
|
if 'http' in cleanup:
|
|
http_dir = os.path.join(
|
|
CONF.deploy.http_root, _FIRMWARE_SUBDIR, node.uuid)
|
|
LOG.debug('For node %(node)s cleaning up files from %(http_dir)s.',
|
|
{'node': node.uuid, 'http_dir': http_dir})
|
|
shutil.rmtree(http_dir, ignore_errors=True)
|
|
|
|
if 'swift' in cleanup:
|
|
swift_api = swift.SwiftAPI()
|
|
container = CONF.redfish.swift_container
|
|
LOG.debug('For node %(node)s cleaning up files from Swift container '
|
|
'%(container)s.',
|
|
{'node': node.uuid, 'container': container})
|
|
objects = swift_api.connection.list_objects(container)
|
|
for o in objects:
|
|
if o.name and o.name.startswith(node.uuid):
|
|
try:
|
|
swift_api.delete_object(container, o.name)
|
|
except exception.SwiftOperationError as error:
|
|
LOG.warning('For node %(node)s failed to clean up '
|
|
'%(object)s. Error: %(error)s',
|
|
{'node': node.uuid, 'object': o.name,
|
|
'error': error})
|