1#
2# Copyright 2006-2009, 2013, 2014 Red Hat, Inc.
3#
4# This work is licensed under the GNU GPLv2 or later.
5# See the COPYING file in the top-level directory.
6
7import os
8
9from . import urldetect
10from . import urlfetcher
11from .installerinject import perform_initrd_injections
12from .. import progress
13from ..devices import DeviceDisk
14from ..logger import log
15from ..osdict import OSDB
16
17
18# Enum of the various install media types we can have
19(MEDIA_DIR,
20 MEDIA_ISO,
21 MEDIA_URL,
22 MEDIA_KERNEL) = range(1, 5)
23
24
25def _is_url(url):
26    return (url.startswith("http://") or
27            url.startswith("https://") or
28            url.startswith("ftp://"))
29
30
31class _LocationData(object):
32    def __init__(self, os_variant, kernel_pairs, os_media, os_tree):
33        self.os_variant = os_variant
34        self.kernel_pairs = kernel_pairs
35        self.os_media = os_media
36        self.os_tree = os_tree
37
38        self.kernel_url_arg = None
39        if self.os_variant:
40            osobj = OSDB.lookup_os(self.os_variant)
41            self.kernel_url_arg = osobj.get_kernel_url_arg()
42
43
44class InstallerTreeMedia(object):
45    """
46    Class representing --location Tree media. Can be one of
47
48      - A network URL: http://dl.fedoraproject.org/...
49      - A local directory
50      - A local .iso file, which will be accessed with isoinfo
51    """
52
53    @staticmethod
54    def validate_path(conn, path):
55        try:
56            dev = DeviceDisk(conn)
57            dev.device = dev.DEVICE_CDROM
58            dev.set_source_path(path)
59            dev.validate()
60            return dev.get_source_path()
61        except Exception as e:
62            log.debug("Error validating install location", exc_info=True)
63            if path.startswith("nfs:"):
64                log.warning("NFS URL installs are no longer supported. "
65                    "Access your install media over an alternate transport "
66                    "like HTTP, or manually mount the NFS share and install "
67                    "from the local directory mount point.")
68
69            msg = (_("Validating install media '%(media)s' failed: %(error)s") %
70                    {"media": str(path), "error": str(e)})
71            raise ValueError(msg) from None
72
73    @staticmethod
74    def get_system_scratchdir(guest):
75        """
76        Return the tmpdir that's accessible by VMs on system libvirt URIs
77        """
78        if guest.conn.is_xen():
79            return "/var/lib/xen"
80        return "/var/lib/libvirt/boot"
81
82    @staticmethod
83    def make_scratchdir(guest):
84        """
85        Determine the scratchdir for this URI, create it if necessary.
86        scratchdir is the directory that's accessible by VMs
87        """
88        user_scratchdir = os.path.join(
89                guest.conn.get_app_cache_dir(), "boot")
90        system_scratchdir = InstallerTreeMedia.get_system_scratchdir(guest)
91
92        # If we are a session URI, or we don't have access to the system
93        # scratchdir, make sure the session scratchdir exists and use that.
94        if (guest.conn.is_unprivileged() or
95            not os.path.exists(system_scratchdir) or
96            not os.access(system_scratchdir, os.W_OK)):
97            os.makedirs(user_scratchdir, 0o751, exist_ok=True)
98            return user_scratchdir
99
100        return system_scratchdir  # pragma: no cover
101
102    def __init__(self, conn, location, location_kernel, location_initrd,
103                install_kernel, install_initrd, install_kernel_args):
104        self.conn = conn
105        self.location = location
106        self._location_kernel = location_kernel
107        self._location_initrd = location_initrd
108        self._install_kernel = install_kernel
109        self._install_initrd = install_initrd
110        self._install_kernel_args = install_kernel_args
111        self._initrd_injections = []
112        self._extra_args = []
113
114        if location_kernel or location_initrd:
115            if not location:
116                raise ValueError(_("location kernel/initrd may only "
117                    "be specified with a location URL/path"))
118            if not (location_kernel and location_initrd):
119                raise ValueError(_("location kernel/initrd must be "
120                    "be specified as a pair"))
121
122        self._cached_fetcher = None
123        self._cached_data = None
124
125        self._tmpfiles = []
126
127        if self._install_kernel or self._install_initrd:
128            self._media_type = MEDIA_KERNEL
129        elif (not self.conn.is_remote() and
130              os.path.exists(self.location) and
131              os.path.isdir(self.location)):
132            self.location = os.path.abspath(self.location)
133            self._media_type = MEDIA_DIR
134        elif _is_url(self.location):
135            self._media_type = MEDIA_URL
136        else:
137            self._media_type = MEDIA_ISO
138
139        if (self.conn.is_remote() and
140                not self._media_type == MEDIA_URL and
141            not self._media_type == MEDIA_KERNEL):
142            raise ValueError(_("Cannot access install tree on remote "
143                "connection: %s") % self.location)
144
145        if self._media_type == MEDIA_ISO:
146            InstallerTreeMedia.validate_path(self.conn, self.location)
147
148
149    ########################
150    # Install preparations #
151    ########################
152
153    def _get_fetcher(self, guest, meter):
154        meter = progress.ensure_meter(meter)
155
156        if not self._cached_fetcher:
157            scratchdir = InstallerTreeMedia.make_scratchdir(guest)
158
159            if self._media_type == MEDIA_KERNEL:
160                self._cached_fetcher = urlfetcher.DirectFetcher(
161                    None, scratchdir, meter)
162            else:
163                self._cached_fetcher = urlfetcher.fetcherForURI(
164                    self.location, scratchdir, meter)
165
166        self._cached_fetcher.meter = meter
167        return self._cached_fetcher
168
169    def _get_cached_data(self, guest, fetcher):
170        if self._cached_data:
171            return self._cached_data
172
173        store = None
174        os_variant = None
175        os_media = None
176        os_tree = None
177        kernel_paths = []
178        has_location_kernel = bool(
179                self._location_kernel and self._location_initrd)
180
181        if self._media_type == MEDIA_KERNEL:
182            kernel_paths = [
183                    (self._install_kernel, self._install_initrd)]
184        else:
185            store = urldetect.getDistroStore(guest, fetcher,
186                    skip_error=has_location_kernel)
187
188        if store:
189            kernel_paths = store.get_kernel_paths()
190            os_variant = store.get_osdict_info()
191            os_media = store.get_os_media()
192            os_tree = store.get_os_tree()
193        if has_location_kernel:
194            kernel_paths = [
195                    (self._location_kernel, self._location_initrd)]
196
197        self._cached_data = _LocationData(os_variant, kernel_paths,
198                os_media, os_tree)
199        return self._cached_data
200
201    def _prepare_kernel_url(self, guest, cache, fetcher):
202        ignore = guest
203
204        def _check_kernel_pairs():
205            for kpath, ipath in cache.kernel_pairs:
206                if fetcher.hasFile(kpath) and fetcher.hasFile(ipath):
207                    return kpath, ipath
208            raise RuntimeError(  # pragma: no cover
209                    _("Couldn't find kernel for install tree."))
210
211        kernelpath, initrdpath = _check_kernel_pairs()
212        kernel = fetcher.acquireFile(kernelpath)
213        self._tmpfiles.append(kernel)
214        initrd = fetcher.acquireFile(initrdpath)
215        self._tmpfiles.append(initrd)
216
217        perform_initrd_injections(initrd,
218                                  self._initrd_injections,
219                                  fetcher.scratchdir)
220
221        return kernel, initrd
222
223
224    ##############
225    # Public API #
226    ##############
227
228    def _prepare_unattended_data(self, scripts):
229        if not scripts:
230            return
231
232        for script in scripts:
233            expected_filename = script.get_expected_filename()
234            scriptpath = script.write()
235            self._tmpfiles.append(scriptpath)
236            self._initrd_injections.append((scriptpath, expected_filename))
237
238    def _prepare_kernel_url_arg(self, guest, cache):
239        os_variant = cache.os_variant or guest.osinfo.name
240        osobj = OSDB.lookup_os(os_variant)
241        return osobj.get_kernel_url_arg()
242
243    def _prepare_kernel_args(self, guest, cache, unattended_scripts):
244        install_args = None
245        if unattended_scripts:
246            args = []
247            for unattended_script in unattended_scripts:
248                cmdline = unattended_script.generate_cmdline()
249                if cmdline:
250                    args.append(cmdline)
251            install_args = (" ").join(args)
252            log.debug("Generated unattended cmdline: %s", install_args)
253        elif self.is_network_url():
254            kernel_url_arg = self._prepare_kernel_url_arg(guest, cache)
255            if kernel_url_arg:
256                install_args = "%s=%s" % (kernel_url_arg, self.location)
257
258        if install_args:
259            self._extra_args.append(install_args)
260
261        if self._install_kernel_args:
262            ret = self._install_kernel_args
263        else:
264            ret = " ".join(self._extra_args)
265
266        if self._media_type == MEDIA_DIR and not ret:
267            log.warning(_("Directory tree installs typically do not work "
268                "unless extra kernel args are passed to point the "
269                "installer at a network accessible install tree."))
270        return ret
271
272    def prepare(self, guest, meter, unattended_scripts):
273        fetcher = self._get_fetcher(guest, meter)
274        cache = self._get_cached_data(guest, fetcher)
275
276        self._prepare_unattended_data(unattended_scripts)
277        kernel_args = self._prepare_kernel_args(guest, cache, unattended_scripts)
278
279        kernel, initrd = self._prepare_kernel_url(guest, cache, fetcher)
280        return kernel, initrd, kernel_args
281
282    def cleanup(self, guest):
283        ignore = guest
284        for f in self._tmpfiles:
285            log.debug("Removing %s", str(f))
286            os.unlink(f)
287
288        self._tmpfiles = []
289
290    def set_initrd_injections(self, initrd_injections):
291        self._initrd_injections = initrd_injections
292
293    def set_extra_args(self, extra_args):
294        self._extra_args = extra_args
295
296    def cdrom_path(self):
297        if self._media_type in [MEDIA_ISO]:
298            return self.location
299
300    def is_network_url(self):
301        if self._media_type in [MEDIA_URL]:
302            return self.location
303
304    def detect_distro(self, guest):
305        fetcher = self._get_fetcher(guest, None)
306        cache = self._get_cached_data(guest, fetcher)
307        return cache.os_variant
308
309    def get_os_media(self, guest, meter):
310        fetcher = self._get_fetcher(guest, meter)
311        cache = self._get_cached_data(guest, fetcher)
312        return cache.os_media
313
314    def get_os_tree(self, guest, meter):
315        fetcher = self._get_fetcher(guest, meter)
316        cache = self._get_cached_data(guest, fetcher)
317        return cache.os_tree
318
319    def requires_internet(self, guest, meter):
320        if self._media_type in [MEDIA_URL, MEDIA_DIR]:
321            return True
322
323        os_media = self.get_os_media(guest, meter)
324        if os_media:
325            return os_media.is_netinst()
326        return False
327