Skip to content

Commit eed2a8a

Browse files
konan-abhiMridula Joshi
authored andcommitted
Add iso file format inspector
This change excludes image conversion if source image format is ISO. This change includes unit tests for the ISO format inspector using mkisofs to generate the iso files. A test for stashing qcow content in the system_area of an iso file is also included. This change modifies format_inspector.detect_file_format to evaluate all inspectors until they are complete and raise an InvalidDiskInfo exception if multiple formats match. Related-Bug: #2059809 Change-Id: Id706480e31687d8ade6f7199b600aff3ad7c68f7 (cherry picked from commit d8de63a)
1 parent a775ab9 commit eed2a8a

File tree

6 files changed

+345
-31
lines changed

6 files changed

+345
-31
lines changed

glance/async_/flows/plugins/image_conversion.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,24 +90,15 @@ def _execute(self, action, file_path, **kwargs):
9090
dest_path = "%(path)s.%(target)s" % {'path': self.src_path,
9191
'target': target_format}
9292
self.dest_path = dest_path
93-
9493
source_format = action.image_disk_format
95-
inspector_cls = format_inspector.get_inspector(source_format)
96-
if not inspector_cls:
97-
# We cannot convert from disk_format types that qemu-img doesn't
98-
# support (like iso, ploop, etc). The ones it supports overlaps
99-
# with the ones we have inspectors for, so reject conversion for
100-
# any format we don't have an inspector for.
101-
raise RuntimeError(
102-
'Unable to convert from format %s' % source_format)
10394

10495
# Use our own cautious inspector module (if we have one for this
10596
# format) to make sure a file is the format the submitter claimed
10697
# it is and that it passes some basic safety checks _before_ we run
10798
# qemu-img on it.
10899
# See https://bugs.launchpad.net/nova/+bug/2059809 for details.
109100
try:
110-
inspector = inspector_cls.from_file(self.src_path)
101+
inspector = format_inspector.detect_file_format(self.src_path)
111102
if not inspector.safety_check():
112103
LOG.error('Image failed %s safety check; aborting conversion',
113104
source_format)
@@ -122,6 +113,24 @@ def _execute(self, action, file_path, **kwargs):
122113
LOG.exception('Unknown error inspecting image format: %s', e)
123114
raise RuntimeError('Unable to inspect image')
124115

116+
if str(inspector) == 'iso':
117+
if source_format == 'iso':
118+
# NOTE(abhishekk): Excluding conversion and preserving image
119+
# disk_format as `iso` only
120+
LOG.debug("Avoiding conversion of an image %s having"
121+
" `iso` disk format.", self.image_id)
122+
return file_path
123+
124+
# NOTE(abhishekk): Raising error as image detected as ISO but
125+
# claimed as different format
126+
LOG.error('Image claimed to be %s format but format '
127+
'inspection found: ISO', source_format)
128+
raise RuntimeError("Image has disallowed configuration")
129+
elif str(inspector) != source_format:
130+
LOG.error('Image claimed to be %s format failed format '
131+
'inspection', source_format)
132+
raise RuntimeError('Image format mismatch')
133+
125134
try:
126135
stdout, stderr = putils.trycmd("qemu-img", "info",
127136
"-f", source_format,

glance/common/format_inspector.py

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import struct
2525

2626
from oslo_log import log as logging
27+
from oslo_utils import units
2728

2829
LOG = logging.getLogger(__name__)
2930

@@ -839,6 +840,84 @@ def __str__(self):
839840
return 'vdi'
840841

841842

843+
class ISOInspector(FileInspector):
844+
"""ISO 9660 and UDF format
845+
we need to check the first 32KB + descriptor size
846+
to look for the ISO 9660 or UDF signature.
847+
http://wiki.osdev.org/ISO_9660
848+
http://wiki.osdev.org/UDF
849+
mkisofs --help | grep udf
850+
The Universal Disc Format or UDF is the filesystem used on DVDs and
851+
Blu-Ray discs.UDF is an extension of ISO 9660 and shares the same
852+
header structure and initial layout.
853+
Like the CDFS(ISO 9660) file system,
854+
the UDF file system uses a 2048 byte sector size,
855+
and it designates that the first 16 sectors can be used by the OS
856+
to store proprietary data or boot logic.
857+
That means we need to check the first 32KB + descriptor size
858+
to look for the ISO 9660 or UDF signature.
859+
both formats have an extent based layout, so we can't determine
860+
ahead of time where the descriptor will be located.
861+
fortunately, the ISO 9660 and UDF formats have a Primary Volume Descriptor
862+
located at the beginning of the image, which contains the volume size.
863+
"""
864+
def __init__(self, *a, **k):
865+
super(ISOInspector, self).__init__(*a, **k)
866+
self.new_region('system_area', CaptureRegion(0, 32 * units.Ki))
867+
self.new_region('header', CaptureRegion(32 * units.Ki, 2 * units.Ki))
868+
869+
@property
870+
def format_match(self):
871+
if not self.complete:
872+
return False
873+
signature = self.region('header').data[1:6]
874+
assert len(signature) == 5
875+
# These signatures are for ISO9660, UDF and UDF.
876+
return signature in (b'CD001', b'NSR02', b'NSR03')
877+
878+
@property
879+
def virtual_size(self):
880+
if not self.complete:
881+
return 0
882+
if not self.format_match:
883+
return 0
884+
# the header size is 2KB or 1 sector
885+
# the first header field is the descriptor type which is 1 byte
886+
# the second field is the standard identifier which is 5 bytes
887+
# the third field is the version which is 1 byte
888+
# the rest of the header contains type specific data is 2041 bytes
889+
# see http://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor
890+
# we need to check that the descriptor type is 1
891+
# to ensure that this is a primary volume descriptor
892+
descriptor_type = self.region('header').data[0]
893+
if descriptor_type != 1:
894+
return 0
895+
# The size in bytes of a logical block is stored at offset 128
896+
# and is 2 bytes long encoded in both little and big endian
897+
# int16_LSB-MSB so the field is 4 bytes long
898+
logical_block_size_data = self.region('header').data[128:132]
899+
assert len(logical_block_size_data) == 4
900+
# given the encoding we only need to read half the field so we
901+
# can use the first 2 bytes which are the little endian part
902+
# this is normally 2048 or 2KB but we need to check as it can be
903+
# different according to the ISO 9660 standard.
904+
logical_block_size, = struct.unpack('<H', logical_block_size_data[:2])
905+
# The volume space size is the total number of logical blocks
906+
# and is stored at offset 80 and is 8 bytes long
907+
# as with the logical block size the field is encoded in both
908+
# little and big endian as an int32_LSB-MSB
909+
volume_space_size_data = self.region('header').data[80:88]
910+
assert len(volume_space_size_data) == 8
911+
# given the encoding we only need to read half the field so we
912+
# can use the first 4 bytes which are the little endian part
913+
volume_space_size, = struct.unpack('<L', volume_space_size_data[:4])
914+
# the virtual size is the volume space size * logical block size
915+
return volume_space_size * logical_block_size
916+
917+
def __str__(self):
918+
return 'iso'
919+
920+
842921
class InfoWrapper(object):
843922
"""A file-like object that wraps another and updates a format inspector.
844923
@@ -891,6 +970,7 @@ def close(self):
891970
'vmdk': VMDKInspector,
892971
'vdi': VDIInspector,
893972
'qed': QEDInspector,
973+
'iso': ISOInspector,
894974
}
895975

896976

@@ -906,14 +986,16 @@ def get_inspector(format_name):
906986

907987
def detect_file_format(filename):
908988
"""Attempts to detect the format of a file.
909-
910989
This runs through a file one time, running all the known inspectors in
911-
parallel. It stops reading the file once one of them matches or all of
990+
parallel. It stops reading the file once all of them matches or all of
912991
them are sure they don't match.
913992
914-
Returns the FileInspector that matched, if any. None if 'raw'.
993+
:param filename: The path to the file to inspect.
994+
:returns: A FormatInspector instance matching the file.
995+
:raises: ImageFormatError if multiple formats are detected.
915996
"""
916997
inspectors = {k: v() for k, v in ALL_FORMATS.items()}
998+
detections = []
917999
with open(filename, 'rb') as f:
9181000
for chunk in chunked_reader(f):
9191001
for format, inspector in list(inspectors.items()):
@@ -925,10 +1007,16 @@ def detect_file_format(filename):
9251007
continue
9261008
if (inspector.format_match and inspector.complete and
9271009
format != 'raw'):
928-
# First complete match (other than raw) wins
929-
return inspector
1010+
# record all match (other than raw)
1011+
detections.append(inspector)
1012+
inspectors.pop(format)
9301013
if all(i.complete for i in inspectors.values()):
9311014
# If all the inspectors are sure they are not a match, avoid
9321015
# reading to the end of the file to settle on 'raw'.
9331016
break
934-
return inspectors['raw']
1017+
1018+
if len(detections) > 1:
1019+
all_formats = [str(inspector) for inspector in detections]
1020+
raise ImageFormatError(
1021+
'Multiple formats detected: %s' % ', '.join(all_formats))
1022+
return inspectors['raw'] if not detections else detections[0]

0 commit comments

Comments
 (0)