Skip to content

Commit 0b3ed09

Browse files
committed
Don't create a hardlink to a symlink when handling file:// URLs
While os.link is supposed to follow symlinks, it's actually broken [1] on Linux. As a result, Ironic may end up creating a hard link to a symlink. If the symlink is relative, chances are high accessing the resulting file will cause a FileNotFoundError. [1] python/cpython#81793 Change-Id: Ic52f0ddb0c94410dd854ee525e3c57b2e78ea84d
1 parent 88fcf8a commit 0b3ed09

File tree

3 files changed

+37
-4
lines changed

3 files changed

+37
-4
lines changed

ironic/common/image_service.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,21 @@ def download(self, image_href, image_file):
322322
image_file.close()
323323
os.remove(dest_image_path)
324324

325+
# NOTE(dtantsur): os.link is supposed to follow symlinks, but it
326+
# does not: https://github.com/python/cpython/issues/81793
327+
real_image_path = os.path.realpath(source_image_path)
325328
try:
326-
os.link(source_image_path, dest_image_path)
329+
os.link(real_image_path, dest_image_path)
327330
except OSError as exc:
328-
LOG.debug('Could not create a link from %(src)s to %(dest)s, '
329-
'will copy the content instead. Error: %(exc)s.',
331+
orig = (f' (real path {real_image_path})'
332+
if real_image_path != source_image_path
333+
else '')
334+
335+
LOG.debug('Could not create a link from %(src)s%(orig)s to '
336+
'%(dest)s, will copy the content instead. '
337+
'Error: %(exc)s.',
330338
{'src': source_image_path, 'dest': dest_image_path,
331-
'exc': exc})
339+
'orig': orig, 'exc': exc})
332340
else:
333341
return
334342

ironic/tests/unit/common/test_image_service.py

+19
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ def test_show(self, _validate_mock, getsize_mock, getmtime_mock):
612612

613613
@mock.patch.object(shutil, 'copyfile', autospec=True)
614614
@mock.patch.object(os, 'link', autospec=True)
615+
@mock.patch.object(os.path, 'realpath', lambda p: p)
615616
@mock.patch.object(os, 'remove', autospec=True)
616617
@mock.patch.object(image_service.FileImageService, 'validate_href',
617618
autospec=True)
@@ -642,6 +643,24 @@ def test_download_copy(self, _validate_mock, remove_mock, link_mock,
642643
link_mock.assert_called_once_with(self.href_path, 'file')
643644
copy_mock.assert_called_once_with(self.href_path, 'file')
644645

646+
@mock.patch.object(shutil, 'copyfile', autospec=True)
647+
@mock.patch.object(os, 'link', autospec=True)
648+
@mock.patch.object(os.path, 'realpath', autospec=True)
649+
@mock.patch.object(os, 'remove', autospec=True)
650+
@mock.patch.object(image_service.FileImageService, 'validate_href',
651+
autospec=True)
652+
def test_download_symlink(self, _validate_mock, remove_mock,
653+
realpath_mock, link_mock, copy_mock):
654+
_validate_mock.return_value = self.href_path
655+
realpath_mock.side_effect = lambda p: p + '.real'
656+
file_mock = mock.MagicMock(spec=io.BytesIO)
657+
file_mock.name = 'file'
658+
self.service.download(self.href, file_mock)
659+
_validate_mock.assert_called_once_with(mock.ANY, self.href)
660+
realpath_mock.assert_called_once_with(self.href_path)
661+
link_mock.assert_called_once_with(self.href_path + '.real', 'file')
662+
copy_mock.assert_not_called()
663+
645664
@mock.patch.object(shutil, 'copyfile', autospec=True)
646665
@mock.patch.object(os, 'link', autospec=True)
647666
@mock.patch.object(os, 'remove', autospec=True)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes the behavior of ``file:///`` image URLs pointing at a symlink.
5+
Ironic no longer creates a hard link to the symlink, which could cause
6+
confusing FileNotFoundError to happen if the symlink is relative.

0 commit comments

Comments
 (0)