2013-03-18 01:06:52 +04:00
#
2013-10-28 00:59:47 +04:00
# Copyright 2006-2007, 2013 Red Hat, Inc.
2013-03-18 01:06:52 +04:00
#
2018-04-04 16:35:41 +03:00
# This work is licensed under the GNU GPLv2 or later.
2018-03-20 22:00:02 +03:00
# See the COPYING file in the top-level directory.
2019-06-08 03:55:11 +03:00
#
# Backends for the various URL types we support (http, https, ftp, local)
2013-03-18 01:06:52 +04:00
2013-08-09 04:14:10 +04:00
import ftplib
2017-10-11 14:35:50 +03:00
import io
2013-03-18 01:06:52 +04:00
import os
2013-08-09 04:14:10 +04:00
import subprocess
import tempfile
2018-04-03 18:38:37 +03:00
import urllib
2013-08-09 04:14:10 +04:00
2016-04-18 23:42:12 +03:00
import requests
2019-06-17 04:34:47 +03:00
from . . logger import log
2019-06-17 04:12:39 +03:00
2013-04-13 22:34:52 +04:00
2021-04-07 16:51:41 +03:00
#########################
# isoreader abstraction #
#########################
class _ISOReader :
def __init__ ( self , location ) :
self . _location = location
2021-04-07 18:45:00 +03:00
def grabFile ( self , url , scratchdir ) :
2021-04-07 16:51:41 +03:00
raise NotImplementedError ( )
def hasFile ( self , url ) :
raise NotImplementedError ( )
class _ISOinfoReader ( _ISOReader ) :
"""
Handle reading reading files off an iso
"""
def __init__ ( self , location ) :
super ( ) . __init__ ( location )
self . _cache_file_list = self . _make_file_list ( )
def _make_file_list ( self ) :
cmd = [ " isoinfo " , " -J " , " -i " , self . _location , " -f " ]
2021-04-07 18:45:00 +03:00
log . debug ( " Generating iso filelist: %s " , cmd )
2021-04-07 16:51:41 +03:00
output = subprocess . check_output ( cmd , stderr = subprocess . DEVNULL )
return output . splitlines ( False )
2021-04-07 18:45:00 +03:00
def grabFile ( self , url , scratchdir ) :
ignore = scratchdir
2021-04-07 16:51:41 +03:00
cmd = [ " isoinfo " , " -J " , " -i " , self . _location , " -x " , url ]
2021-04-07 18:45:00 +03:00
log . debug ( " Extracting iso file: %s " , cmd )
2021-04-07 16:51:41 +03:00
return subprocess . check_output ( cmd )
def hasFile ( self , url ) :
return url . encode ( " ascii " ) in self . _cache_file_list
2021-04-07 18:45:00 +03:00
class _XorrisoReader ( _ISOReader ) :
def __init__ ( self , location ) :
super ( ) . __init__ ( location )
self . _cache_file_list = self . _make_file_list ( )
def _make_file_list ( self ) :
delim = " VIRTINST_BEGINLIST "
cmd = [ " xorriso " , " -indev " , self . _location , " -print " , delim , " -find " ]
log . debug ( " Generating iso filelist: %s " , cmd )
output = subprocess . check_output ( cmd ,
stderr = subprocess . DEVNULL , text = True )
return output . split ( delim , 1 ) [ 1 ] . strip ( ) . splitlines ( )
def grabFile ( self , url , scratchdir ) :
tmp = tempfile . NamedTemporaryFile (
prefix = " virtinst-iso " , suffix = " - " + os . path . basename ( url ) ,
dir = scratchdir )
cmd = [ " xorriso " , " -osirrox " , " on " , " -indev " , self . _location ,
" -extract " , url , tmp . name ]
log . debug ( " Extracting iso file: %s " , cmd )
subprocess . check_output ( cmd )
return open ( tmp . name , " rb " ) . read ( )
def hasFile ( self , url ) :
return ( " ' . %s ' " % url ) in self . _cache_file_list
2019-06-08 03:55:11 +03:00
###########################
# Fetcher implementations #
###########################
2013-04-13 22:34:52 +04:00
2015-09-18 23:49:18 +03:00
class _URLFetcher ( object ) :
2013-08-09 04:14:10 +04:00
"""
This is a generic base class for fetching / extracting files from
2018-06-12 20:49:25 +03:00
a media source , such as CD ISO , or HTTP / HTTPS / FTP server
2013-08-09 04:14:10 +04:00
"""
2015-09-19 01:31:56 +03:00
_block_size = 16384
2018-10-09 16:52:23 +03:00
_is_iso = False
2015-09-19 01:31:56 +03:00
2013-09-26 21:04:28 +04:00
def __init__ ( self , location , scratchdir , meter ) :
2013-08-09 04:14:10 +04:00
self . location = location
self . scratchdir = scratchdir
2013-09-26 21:04:28 +04:00
self . meter = meter
2013-08-09 04:14:10 +04:00
2019-06-17 04:12:39 +03:00
log . debug ( " Using scratchdir= %s " , scratchdir )
2019-01-30 01:00:30 +03:00
self . _prepare ( )
2014-02-14 20:02:35 +04:00
2015-09-19 01:31:56 +03:00
2015-09-18 23:49:18 +03:00
####################
# Internal helpers #
####################
def _make_full_url ( self , filename ) :
"""
Generate a full fetchable URL from the passed filename , which
is relative to the self . location
"""
if not filename :
2019-01-29 23:11:03 +03:00
return self . location
return os . path . join ( self . location , filename )
2015-09-18 23:49:18 +03:00
2019-06-13 00:25:56 +03:00
def _grabURL ( self , filename , fileobj , fullurl = None ) :
2015-09-18 23:49:18 +03:00
"""
2015-09-19 01:31:56 +03:00
Download the filename from self . location , and write contents to
fileobj
2015-09-18 23:49:18 +03:00
"""
2019-06-13 00:25:56 +03:00
if fullurl :
url = fullurl
else :
url = self . _make_full_url ( filename )
2015-09-19 01:31:56 +03:00
try :
urlobj , size = self . _grabber ( url )
2017-05-05 19:47:21 +03:00
except Exception as e :
2020-09-15 19:33:31 +03:00
msg = ( _ ( " Couldn ' t acquire file %(url)s : %(error)s " ) % {
" url " : url , " error " : str ( e ) } )
raise ValueError ( msg ) from None
2015-09-19 01:31:56 +03:00
2019-06-17 04:12:39 +03:00
log . debug ( " Fetching URI: %s " , url )
2015-09-19 01:31:56 +03:00
self . meter . start (
text = _ ( " Retrieving file %s ... " ) % os . path . basename ( filename ) ,
size = size )
total = self . _write ( urlobj , fileobj )
self . meter . end ( total )
def _write ( self , urlobj , fileobj ) :
"""
Write the contents of urlobj to python file like object fileobj
"""
total = 0
2015-09-19 00:11:37 +03:00
while 1 :
2015-09-19 01:31:56 +03:00
buff = urlobj . read ( self . _block_size )
2015-09-19 00:11:37 +03:00
if not buff :
break
fileobj . write ( buff )
2015-09-19 01:31:56 +03:00
total + = len ( buff )
self . meter . update ( total )
2019-08-05 14:47:25 +03:00
fileobj . flush ( )
2015-09-19 01:31:56 +03:00
return total
2013-08-09 04:14:10 +04:00
2015-09-19 01:31:56 +03:00
def _grabber ( self , url ) :
2015-09-19 01:01:57 +03:00
"""
2015-09-19 01:31:56 +03:00
Returns the urlobj , size for the passed URL . urlobj is whatever
data needs to be passed to self . _write
2015-09-19 01:01:57 +03:00
"""
2015-09-19 01:31:56 +03:00
raise NotImplementedError ( " must be implemented in subclass " )
2015-09-19 01:01:57 +03:00
2015-09-18 23:49:18 +03:00
##############
# Public API #
##############
2018-10-09 16:52:23 +03:00
def is_iso ( self ) :
"""
If this is a fetcher for local CDROM / ISO
"""
return self . _is_iso
2019-01-30 01:00:30 +03:00
def _prepare ( self ) :
2015-09-18 23:49:18 +03:00
"""
Perform any necessary setup
"""
2013-08-09 04:14:10 +04:00
2019-01-30 01:00:30 +03:00
def _cleanup ( self ) :
2015-09-18 23:49:18 +03:00
"""
Perform any necessary cleanup
"""
2013-08-09 04:14:10 +04:00
2019-01-30 01:00:30 +03:00
def __del__ ( self ) :
self . _cleanup ( )
2018-03-30 02:22:28 +03:00
def can_access ( self ) :
"""
Return True if the location URL seems to be valid
"""
return True
2016-03-24 21:51:36 +03:00
def _hasFile ( self , url ) :
raise NotImplementedError ( " Must be implemented in subclass " )
2015-09-18 23:49:18 +03:00
def hasFile ( self , filename ) :
"""
Return True if self . location has the passed filename
"""
2016-03-24 21:51:36 +03:00
url = self . _make_full_url ( filename )
ret = self . _hasFile ( url )
2019-06-17 04:12:39 +03:00
log . debug ( " hasFile( %s ) returning %s " , url , ret )
2016-03-24 21:51:36 +03:00
return ret
2015-09-18 23:49:18 +03:00
2019-06-13 00:25:56 +03:00
def acquireFile ( self , filename , fullurl = None ) :
2015-09-18 23:49:18 +03:00
"""
Grab the passed filename from self . location and save it to
a temporary file , returning the temp filename
"""
2016-04-18 23:42:12 +03:00
# pylint: disable=redefined-variable-type
2019-06-10 20:10:58 +03:00
fn = None
try :
fileobj = tempfile . NamedTemporaryFile (
2019-06-14 04:50:38 +03:00
prefix = " virtinst- " , suffix = " - " + os . path . basename ( filename ) ,
dir = self . scratchdir , delete = False )
2019-06-10 20:10:58 +03:00
fn = fileobj . name
2019-06-13 00:25:56 +03:00
self . _grabURL ( filename , fileobj , fullurl = fullurl )
2019-06-17 04:12:39 +03:00
log . debug ( " Saved file to %s " , fn )
2019-06-10 20:10:58 +03:00
return fn
2020-01-27 18:44:09 +03:00
except BaseException : # pragma: no cover
if fn and os . path . exists ( fn ) :
os . unlink ( fn )
raise
2015-09-19 01:01:57 +03:00
def acquireFileContent ( self , filename ) :
"""
Grab the passed filename from self . location and return it as a string
"""
2017-11-08 09:23:28 +03:00
fileobj = io . BytesIO ( )
2015-09-19 01:31:56 +03:00
self . _grabURL ( filename , fileobj )
2018-01-27 23:23:22 +03:00
return fileobj . getvalue ( ) . decode ( " utf-8 " )
2013-08-09 04:14:10 +04:00
2015-09-18 23:49:18 +03:00
class _HTTPURLFetcher ( _URLFetcher ) :
2018-03-30 00:56:40 +03:00
_session = None
2019-01-30 01:00:30 +03:00
def _prepare ( self ) :
2020-01-27 18:44:09 +03:00
self . _session = requests . Session ( )
2018-03-30 00:56:40 +03:00
2019-01-30 01:00:30 +03:00
def _cleanup ( self ) :
2018-03-30 00:56:40 +03:00
if self . _session :
try :
self . _session . close ( )
2019-06-10 21:15:50 +03:00
except Exception : # pragma: no cover
2019-06-17 04:12:39 +03:00
log . debug ( " Error closing requests.session " , exc_info = True )
2018-03-30 00:56:40 +03:00
self . _session = None
2018-03-30 02:22:28 +03:00
def can_access ( self ) :
return self . hasFile ( " " )
2016-03-24 21:51:36 +03:00
def _hasFile ( self , url ) :
2015-09-19 01:31:56 +03:00
"""
We just do a HEAD request to see if the file exists
"""
2013-08-09 04:14:10 +04:00
try :
2018-03-30 00:56:40 +03:00
response = self . _session . head ( url , allow_redirects = True )
2015-09-19 01:31:56 +03:00
response . raise_for_status ( )
2019-06-10 21:15:50 +03:00
except Exception as e : # pragma: no cover
2019-06-17 04:12:39 +03:00
log . debug ( " HTTP hasFile request failed: %s " , str ( e ) )
2013-08-09 04:14:10 +04:00
return False
return True
2015-09-19 01:31:56 +03:00
def _grabber ( self , url ) :
"""
Use requests for this
"""
2018-03-30 00:56:40 +03:00
response = self . _session . get ( url , stream = True )
2015-09-19 01:31:56 +03:00
response . raise_for_status ( )
2016-01-12 20:45:02 +03:00
try :
size = int ( response . headers . get ( ' content-length ' ) )
2019-06-10 21:15:50 +03:00
except Exception : # pragma: no cover
2016-01-12 20:45:02 +03:00
size = None
return response , size
2015-09-19 01:31:56 +03:00
def _write ( self , urlobj , fileobj ) :
"""
The requests object doesn ' t have a file-like read() option, so
2018-05-21 22:42:50 +03:00
we need to implement it ourselves
2015-09-19 01:31:56 +03:00
"""
total = 0
for data in urlobj . iter_content ( chunk_size = self . _block_size ) :
fileobj . write ( data )
total + = len ( data )
self . meter . update ( total )
2019-08-05 14:47:25 +03:00
fileobj . flush ( )
2015-09-19 01:31:56 +03:00
return total
2013-08-09 04:14:10 +04:00
2015-09-18 23:49:18 +03:00
class _FTPURLFetcher ( _URLFetcher ) :
_ftp = None
2013-08-09 04:14:10 +04:00
2019-01-30 01:00:30 +03:00
def _prepare ( self ) :
2015-09-18 23:49:18 +03:00
if self . _ftp :
2019-06-10 21:15:50 +03:00
return # pragma: no cover
2014-09-07 22:22:56 +04:00
virtinst: early detect ftp connection errors
It fixes two problems:
i) "ftp://" was accepted as valid URL but then it causes this
exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib64/python2.7/ftplib.py", line 387, in login
resp = self.sendcmd('USER ' + user)
File "/usr/lib64/python2.7/ftplib.py", line 243, in sendcmd
self.putcmd(cmd)
File "/usr/lib64/python2.7/ftplib.py", line 178, in putcmd
self.putline(line)
File "/usr/lib64/python2.7/ftplib.py", line 173, in putline
self.sock.sendall(line)
AttributeError: 'NoneType' object has no attribute 'sendall'
ii) only a cryptic error message "Unable to complete install: '[Errno
-2] Name or service not known'" was showed to users when the DNS
lookup failed. The exception is now intercepted and decorated with
more information.
Closes: https://bugzilla.redhat.com/show_bug.cgi?id=1086554
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
2014-04-14 16:49:21 +04:00
try :
2018-01-27 22:19:12 +03:00
parsed = urllib . parse . urlparse ( self . location )
2020-01-27 18:44:09 +03:00
self . _ftp = ftplib . FTP ( )
2018-04-18 16:23:33 +03:00
username = urllib . parse . unquote ( parsed . username or ' ' )
password = urllib . parse . unquote ( parsed . password or ' ' )
2018-01-27 23:23:22 +03:00
self . _ftp . connect ( parsed . hostname , parsed . port or 0 )
2018-04-18 16:23:33 +03:00
self . _ftp . login ( username , password )
2017-08-17 23:10:46 +03:00
# Force binary mode
self . _ftp . voidcmd ( " TYPE I " )
2019-06-10 21:15:50 +03:00
except Exception as e : # pragma: no cover
2020-09-15 19:33:31 +03:00
msg = ( _ ( " Opening URL %(url)s failed: %(error)s " ) % {
" url " : self . location , " error " : str ( e ) } )
raise ValueError ( msg ) from None
virtinst: early detect ftp connection errors
It fixes two problems:
i) "ftp://" was accepted as valid URL but then it causes this
exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib64/python2.7/ftplib.py", line 387, in login
resp = self.sendcmd('USER ' + user)
File "/usr/lib64/python2.7/ftplib.py", line 243, in sendcmd
self.putcmd(cmd)
File "/usr/lib64/python2.7/ftplib.py", line 178, in putcmd
self.putline(line)
File "/usr/lib64/python2.7/ftplib.py", line 173, in putline
self.sock.sendall(line)
AttributeError: 'NoneType' object has no attribute 'sendall'
ii) only a cryptic error message "Unable to complete install: '[Errno
-2] Name or service not known'" was showed to users when the DNS
lookup failed. The exception is now intercepted and decorated with
more information.
Closes: https://bugzilla.redhat.com/show_bug.cgi?id=1086554
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
2014-04-14 16:49:21 +04:00
2015-09-19 01:31:56 +03:00
def _grabber ( self , url ) :
"""
2017-10-11 14:35:49 +03:00
Use urllib and ftplib to grab the file
2015-09-19 01:31:56 +03:00
"""
2018-01-27 22:19:12 +03:00
request = urllib . request . Request ( url )
urlobj = urllib . request . urlopen ( request )
size = self . _ftp . size ( urllib . parse . urlparse ( url ) [ 2 ] )
2015-09-19 01:31:56 +03:00
return urlobj , size
2019-01-30 01:00:30 +03:00
def _cleanup ( self ) :
2015-09-18 23:49:18 +03:00
if not self . _ftp :
2019-06-10 21:15:50 +03:00
return # pragma: no cover
2014-09-07 22:22:56 +04:00
try :
2015-09-18 23:49:18 +03:00
self . _ftp . quit ( )
2019-06-10 21:15:50 +03:00
except Exception : # pragma: no cover
2019-06-17 04:12:39 +03:00
log . debug ( " Error quitting ftp connection " , exc_info = True )
2014-09-07 22:22:56 +04:00
2015-09-18 23:49:18 +03:00
self . _ftp = None
2013-08-09 04:14:10 +04:00
2016-03-24 21:51:36 +03:00
def _hasFile ( self , url ) :
2018-01-27 22:19:12 +03:00
path = urllib . parse . urlparse ( url ) [ 2 ]
2013-08-09 04:14:10 +04:00
try :
try :
# If it's a file
2015-09-19 01:31:56 +03:00
self . _ftp . size ( path )
2020-01-27 18:44:09 +03:00
except ftplib . all_errors : # pragma: no cover
2013-08-09 04:14:10 +04:00
# If it's a dir
2015-09-19 01:31:56 +03:00
self . _ftp . cwd ( path )
2019-06-10 21:15:50 +03:00
except ftplib . all_errors as e : # pragma: no cover
2019-06-17 04:12:39 +03:00
log . debug ( " FTP hasFile: couldn ' t access %s : %s " ,
2015-09-18 23:49:18 +03:00
url , str ( e ) )
2013-08-09 04:14:10 +04:00
return False
return True
2015-09-18 23:49:18 +03:00
class _LocalURLFetcher ( _URLFetcher ) :
"""
For grabbing files from a local directory
"""
2016-03-24 21:51:36 +03:00
def _hasFile ( self , url ) :
2019-09-24 15:26:43 +03:00
parsed = urllib . parse . urlparse ( url )
return os . path . exists ( parsed . path )
2013-08-09 04:14:10 +04:00
2015-09-19 01:31:56 +03:00
def _grabber ( self , url ) :
2019-09-24 15:26:43 +03:00
parsed = urllib . parse . urlparse ( url )
urlobj = open ( parsed . path , " rb " )
size = os . path . getsize ( parsed . path )
2015-09-19 01:31:56 +03:00
return urlobj , size
2013-08-09 04:14:10 +04:00
2017-11-24 08:11:14 +03:00
class _ISOURLFetcher ( _URLFetcher ) :
2021-04-07 16:51:41 +03:00
_isoreader = None
2018-10-09 16:52:23 +03:00
_is_iso = True
2017-11-24 08:11:14 +03:00
2020-07-18 03:51:54 +03:00
def _make_full_url ( self , filename ) :
return os . path . join ( " / " , filename )
2021-04-07 16:51:41 +03:00
def _get_isoreader ( self ) :
if not self . _isoreader :
2021-04-07 18:45:00 +03:00
self . _isoreader = _XorrisoReader ( self . location )
2021-04-07 16:51:41 +03:00
return self . _isoreader
2017-11-24 08:11:14 +03:00
def _grabber ( self , url ) :
2018-03-28 21:02:23 +03:00
if not self . _hasFile ( url ) :
2021-04-07 16:51:41 +03:00
raise RuntimeError ( " iso doesn ' t have file= %s " % url )
2017-11-24 08:11:14 +03:00
2021-04-07 18:45:00 +03:00
output = self . _get_isoreader ( ) . grabFile ( url , self . scratchdir )
2017-11-24 08:11:14 +03:00
return io . BytesIO ( output ) , len ( output )
def _hasFile ( self , url ) :
2021-04-07 16:51:41 +03:00
return self . _get_isoreader ( ) . hasFile ( url )
2017-11-24 08:11:14 +03:00
2019-06-13 00:25:56 +03:00
class DirectFetcher ( _URLFetcher ) :
def _make_full_url ( self , filename ) :
return filename
def acquireFile ( self , filename , fullurl = None ) :
2019-08-02 18:01:44 +03:00
if not fullurl :
fullurl = filename
2019-06-13 00:25:56 +03:00
filename = os . path . basename ( filename )
fetcher = fetcherForURI ( fullurl , self . scratchdir , self . meter , direct = True )
return fetcher . acquireFile ( filename , fullurl ) # pylint: disable=protected-access
def _hasFile ( self , url ) :
return True
def _grabber ( self , url ) :
raise RuntimeError ( # pragma: no cover
" DirectFetcher shouldn ' t be used for file access. " )
def fetcherForURI ( uri , scratchdir , meter , direct = False ) :
2013-09-26 18:24:28 +04:00
if uri . startswith ( " http:// " ) or uri . startswith ( " https:// " ) :
2015-09-18 23:49:18 +03:00
fclass = _HTTPURLFetcher
2013-03-18 01:06:52 +04:00
elif uri . startswith ( " ftp:// " ) :
2015-09-18 23:49:18 +03:00
fclass = _FTPURLFetcher
2019-06-13 00:25:56 +03:00
elif direct or os . path . isdir ( uri ) :
2015-09-18 23:49:18 +03:00
# Pointing to a local tree
fclass = _LocalURLFetcher
2013-03-18 01:06:52 +04:00
else :
2017-11-24 08:11:14 +03:00
# Pointing to a path (e.g. iso), or a block device (e.g. /dev/cdrom)
fclass = _ISOURLFetcher
2019-06-13 00:25:56 +03:00
return fclass ( uri , scratchdir , meter )