~ubuntu-core-dev/ubuntu-release-upgrader/trunk

« back to all changes in this revision

Viewing changes to DistUpgrade/DistUpgradeViewNonInteractive.py

  • Committer: Balint Reczey
  • Date: 2019-12-17 20:29:55 UTC
  • Revision ID: balint.reczey@canonical.com-20191217202955-nqe4xz2c54s60y59
Moved to git at https://git.launchpad.net/ubuntu-release-upgrader

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# DistUpgradeView.py 
2
 
#  
3
 
#  Copyright (c) 2004,2005 Canonical
4
 
#  
5
 
#  Author: Michael Vogt <michael.vogt@ubuntu.com>
6
 
7
 
#  This program is free software; you can redistribute it and/or 
8
 
#  modify it under the terms of the GNU General Public License as 
9
 
#  published by the Free Software Foundation; either version 2 of the
10
 
#  License, or (at your option) any later version.
11
 
12
 
#  This program is distributed in the hope that it will be useful,
13
 
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 
#  GNU General Public License for more details.
16
 
17
 
#  You should have received a copy of the GNU General Public License
18
 
#  along with this program; if not, write to the Free Software
19
 
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
20
 
#  USA
21
 
 
22
 
import apt
23
 
import apt_pkg
24
 
import logging
25
 
import locale
26
 
import time
27
 
import sys
28
 
import os
29
 
import pty
30
 
import select
31
 
import subprocess
32
 
import copy
33
 
import apt.progress
34
 
 
35
 
from configparser import NoSectionError, NoOptionError
36
 
from subprocess import PIPE, Popen
37
 
 
38
 
from .DistUpgradeView import DistUpgradeView, InstallProgress, AcquireProgress
39
 
from .telemetry import get as get_telemetry
40
 
from .DistUpgradeConfigParser import DistUpgradeConfig
41
 
 
42
 
 
43
 
class NonInteractiveAcquireProgress(AcquireProgress):
44
 
    def update_status(self, uri, descr, shortDescr, status):
45
 
        AcquireProgress.update_status(self, uri, descr, shortDescr, status)
46
 
        #logging.debug("Fetch: updateStatus %s %s" % (uri, status))
47
 
        if status == apt_pkg.STAT_DONE:
48
 
            print("fetched %s (%.2f/100) at %sb/s" % (
49
 
                uri, self.percent, apt_pkg.size_to_str(int(self.current_cps))))
50
 
            if sys.stdout.isatty():
51
 
                sys.stdout.flush()
52
 
        
53
 
 
54
 
class NonInteractiveInstallProgress(InstallProgress):
55
 
    """ 
56
 
    Non-interactive version of the install progress class
57
 
    
58
 
    This ensures that conffile prompts are handled and that
59
 
    hanging scripts are killed after a (long) timeout via ctrl-c
60
 
    """
61
 
 
62
 
    def __init__(self, logdir):
63
 
        InstallProgress.__init__(self)
64
 
        logging.debug("setting up environ for non-interactive use")
65
 
        if "DEBIAN_FRONTEND" not in os.environ:
66
 
            os.environ["DEBIAN_FRONTEND"] = "noninteractive"
67
 
        os.environ["APT_LISTCHANGES_FRONTEND"] = "none"
68
 
        os.environ["RELEASE_UPRADER_NO_APPORT"] = "1"
69
 
        self.config = DistUpgradeConfig(".")
70
 
        self.logdir = logdir
71
 
        self.install_run_number = 0
72
 
        try:
73
 
            if self.config.getWithDefault("NonInteractive","ForceOverwrite", False):
74
 
                apt_pkg.config.set("DPkg::Options::","--force-overwrite")
75
 
        except (NoSectionError, NoOptionError):
76
 
            pass
77
 
        # more debug
78
 
        #apt_pkg.config.set("Debug::pkgOrderList","true")
79
 
        #apt_pkg.config.set("Debug::pkgDPkgPM","true")
80
 
        # default to 2400 sec timeout
81
 
        self.timeout = 2400
82
 
        try:
83
 
            self.timeout = self.config.getint("NonInteractive","TerminalTimeout")
84
 
        except Exception:
85
 
            pass
86
 
 
87
 
    def error(self, pkg, errormsg):
88
 
        logging.error("got a error from dpkg for pkg: '%s': '%s'" % (pkg, errormsg))
89
 
        # check if re-run of maintainer script is requested
90
 
        if not self.config.getWithDefault(
91
 
            "NonInteractive","DebugBrokenScripts", False):
92
 
            return
93
 
        # re-run maintainer script with sh -x/perl debug to get a better 
94
 
        # idea what went wrong
95
 
        # 
96
 
        # FIXME: this is just a approximation for now, we also need
97
 
        #        to pass:
98
 
        #        - a version after remove (if upgrade to new version)
99
 
        #
100
 
        #        not everything is a shell or perl script
101
 
        #
102
 
        # if the new preinst fails, its not yet in /var/lib/dpkg/info
103
 
        # so this is inaccurate as well
104
 
        environ = copy.copy(os.environ)
105
 
        environ["PYCENTRAL"] = "debug"
106
 
        cmd = []
107
 
 
108
 
        # find what maintainer script failed
109
 
        if "post-installation" in errormsg:
110
 
            prefix = "/var/lib/dpkg/info/"
111
 
            name = "postinst"
112
 
            argument = "configure"
113
 
            maintainer_script = "%s/%s.%s" % (prefix, pkg, name)
114
 
        elif "pre-installation" in errormsg:
115
 
            prefix = "/var/lib/dpkg/tmp.ci/"
116
 
            #prefix = "/var/lib/dpkg/info/"
117
 
            name = "preinst"
118
 
            argument = "install"
119
 
            maintainer_script = "%s/%s" % (prefix, name)
120
 
        elif "pre-removal" in errormsg:
121
 
            prefix = "/var/lib/dpkg/info/"
122
 
            name = "prerm"
123
 
            argument = "remove"
124
 
            maintainer_script = "%s/%s.%s" % (prefix, pkg, name)
125
 
        elif "post-removal" in errormsg:
126
 
            prefix = "/var/lib/dpkg/info/"
127
 
            name = "postrm"
128
 
            argument = "remove"
129
 
            maintainer_script = "%s/%s.%s" % (prefix, pkg, name)
130
 
        else:
131
 
            print("UNKNOWN (trigger?) dpkg/script failure for %s (%s) " % (pkg, errormsg))
132
 
            return
133
 
 
134
 
        # find out about the interpreter
135
 
        if not os.path.exists(maintainer_script):
136
 
            logging.error("can not find failed maintainer script '%s' " % maintainer_script)
137
 
            return
138
 
        with open(maintainer_script) as f:
139
 
            interp = f.readline()[2:].strip().split()[0]
140
 
        if ("bash" in interp) or ("/bin/sh" in interp):
141
 
            debug_opts = ["-ex"]
142
 
        elif ("perl" in interp):
143
 
            debug_opts = ["-d"]
144
 
            environ["PERLDB_OPTS"] = "AutoTrace NonStop"
145
 
        else:
146
 
            logging.warning("unknown interpreter: '%s'" % interp)
147
 
 
148
 
        # check if debconf is used and fiddle a bit more if it is
149
 
        with open(maintainer_script) as f:
150
 
            maintainer_script_text = f.read()
151
 
        if ". /usr/share/debconf/confmodule" in maintainer_script_text:
152
 
            environ["DEBCONF_DEBUG"] = "developer"
153
 
            environ["DEBIAN_HAS_FRONTEND"] = "1"
154
 
            interp = "/usr/share/debconf/frontend"
155
 
            debug_opts = ["sh","-ex"]
156
 
 
157
 
        # build command
158
 
        cmd.append(interp)
159
 
        cmd.extend(debug_opts)
160
 
        cmd.append(maintainer_script)
161
 
        cmd.append(argument)
162
 
 
163
 
        # check if we need to pass a version
164
 
        if name == "postinst":
165
 
            version = Popen("dpkg-query -s %s|grep ^Config-Version" % pkg,
166
 
                            shell=True, stdout=PIPE,
167
 
                            universal_newlines=True).communicate()[0]
168
 
            if version:
169
 
                cmd.append(version.split(":",1)[1].strip())
170
 
        elif name == "preinst":
171
 
            pkg = os.path.basename(pkg)
172
 
            pkg = pkg.split("_")[0]
173
 
            version = Popen("dpkg-query -s %s|grep ^Version" % pkg,
174
 
                            shell=True, stdout=PIPE,
175
 
                            universal_newlines=True).communicate()[0]
176
 
            if version:
177
 
                cmd.append(version.split(":",1)[1].strip())
178
 
 
179
 
        logging.debug("re-running '%s' (%s)" % (cmd, environ))
180
 
        ret = subprocess.call(cmd, env=environ)
181
 
        logging.debug("%s script returned: %s" % (name,ret))
182
 
 
183
 
    def conffile(self, current, new):
184
 
        logging.warning("got a conffile-prompt from dpkg for file: '%s'" %
185
 
                        current)
186
 
        # looks like we have a race here *sometimes*
187
 
        time.sleep(5)
188
 
        try:
189
 
          # don't overwrite
190
 
          os.write(self.master_fd, b"n\n")
191
 
          logging.warning("replied no to the conffile-prompt for file: '%s'" %
192
 
                          current)
193
 
        except Exception as e:
194
 
          logging.error("error '%s' when trying to write to the conffile"%e)
195
 
 
196
 
    def start_update(self):
197
 
        InstallProgress.start_update(self)
198
 
        self.last_activity = time.time()
199
 
        progress_log = self.config.getWithDefault("NonInteractive","DpkgProgressLog", False)
200
 
        if progress_log:
201
 
            fullpath = os.path.join(self.logdir, "dpkg-progress.%s.log" % self.install_run_number)
202
 
            logging.debug("writing dpkg progress log to '%s'" % fullpath)
203
 
            self.dpkg_progress_log = open(fullpath, "w")
204
 
        else:
205
 
            self.dpkg_progress_log = open(os.devnull, "w")
206
 
        self.dpkg_progress_log.write("%s: Start\n" % time.time())
207
 
    def finish_update(self):
208
 
        InstallProgress.finish_update(self)
209
 
        self.dpkg_progress_log.write("%s: Finished\n" % time.time())
210
 
        self.dpkg_progress_log.close()
211
 
        self.install_run_number += 1
212
 
    def status_change(self, pkg, percent, status_str):
213
 
        self.dpkg_progress_log.write("%s:%s:%s:%s\n" % (time.time(),
214
 
                                                        percent,
215
 
                                                        pkg,
216
 
                                                        status_str))
217
 
    def update_interface(self):
218
 
        InstallProgress.update_interface(self)
219
 
        if self.statusfd == None:
220
 
            return
221
 
        if (self.last_activity + self.timeout) < time.time():
222
 
            logging.warning("no activity %s seconds (%s) - sending ctrl-c" % (
223
 
                    self.timeout, self.status))
224
 
            # ctrl-c
225
 
            os.write(self.master_fd,chr(3))
226
 
        # read master fd and write to stdout so that terminal output
227
 
        # actualy works
228
 
        res = select.select([self.master_fd],[],[],0.1)
229
 
        while len(res[0]) > 0:
230
 
           self.last_activity = time.time()
231
 
           try:
232
 
               s = os.read(self.master_fd, 1)
233
 
               sys.stdout.write("%s" % s.decode(
234
 
                    locale.getpreferredencoding(), errors='ignore'))
235
 
           except OSError:
236
 
               # happens after we are finished because the fd is closed
237
 
               return
238
 
           res = select.select([self.master_fd],[],[],0.1)
239
 
        sys.stdout.flush()
240
 
    
241
 
 
242
 
    def fork(self):
243
 
        logging.debug("doing a pty.fork()")
244
 
        # some maintainer scripts fail without
245
 
        os.environ["TERM"] = "dumb"
246
 
        # unset PAGER so that we can do "diff" in the dpkg prompt
247
 
        os.environ["PAGER"] = "true"
248
 
        (self.pid, self.master_fd) = pty.fork()
249
 
        if self.pid != 0:
250
 
            logging.debug("pid is: %s" % self.pid)
251
 
        return self.pid
252
 
 
253
 
 
254
 
class DistUpgradeViewNonInteractive(DistUpgradeView):
255
 
    " non-interactive version of the upgrade view "
256
 
    def __init__(self, datadir=None, logdir=None):
257
 
        DistUpgradeView.__init__(self)
258
 
        get_telemetry().set_updater_type('NonInteractive')
259
 
        self.config = DistUpgradeConfig(".")
260
 
        self._acquireProgress = NonInteractiveAcquireProgress()
261
 
        self._installProgress = NonInteractiveInstallProgress(logdir)
262
 
        self._opProgress = apt.progress.base.OpProgress()
263
 
        sys.__excepthook__ = self.excepthook
264
 
    def excepthook(self, type, value, tb):
265
 
        " on uncaught exceptions -> print error and reboot "
266
 
        import traceback
267
 
        logging.exception("got exception '%s': %s " % (type, value))
268
 
        lines = traceback.format_exception(type, value, tb)
269
 
        logging.error("not handled exception:\n%s" % "".join(lines))
270
 
        #sys.excepthook(type, value, tb)
271
 
        self.confirmRestart()
272
 
    def getOpCacheProgress(self):
273
 
        " return a OpProgress() subclass for the given graphic"
274
 
        return self._opProgress
275
 
    def getAcquireProgress(self):
276
 
        " return an acquire progress object "
277
 
        return self._acquireProgress
278
 
    def getInstallProgress(self, cache=None):
279
 
        " return a install progress object "
280
 
        return self._installProgress
281
 
    def updateStatus(self, msg):
282
 
        """ update the current status of the distUpgrade based
283
 
            on the current view
284
 
        """
285
 
        pass
286
 
    def setStep(self, step):
287
 
        """ we have 5 steps current for a upgrade:
288
 
        1. Analyzing the system
289
 
        2. Updating repository information
290
 
        3. Performing the upgrade
291
 
        4. Post upgrade stuff
292
 
        5. Complete
293
 
        """
294
 
        super(DistUpgradeViewNonInteractive, self).setStep(step)
295
 
        pass
296
 
    def confirmChanges(self, summary, changes, demotions, downloadSize,
297
 
                       actions=None, removal_bold=True):
298
 
        DistUpgradeView.confirmChanges(self, summary, changes, demotions, 
299
 
                                       downloadSize, actions)
300
 
        logging.debug("toinstall: '%s'" % [p.name for p in self.toInstall])
301
 
        logging.debug("toupgrade: '%s'" % [p.name for p in self.toUpgrade])
302
 
        logging.debug("toremove: '%s'" % [p.name for p in self.toRemove])
303
 
        return True
304
 
    def askYesNoQuestion(self, summary, msg, default='No'):
305
 
        " ask a Yes/No question and return True on 'Yes' "
306
 
        # if this gets enabled upgrades over ssh with the non-interactive
307
 
        # frontend will no longer work
308
 
        #if default.lower() == "no":
309
 
        #    return False
310
 
        return True
311
 
    def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
312
 
        return True
313
 
    def confirmRestart(self):
314
 
        " generic ask about the restart, can be overridden "
315
 
        logging.debug("confirmRestart() called")
316
 
        # rebooting here makes sense if we run e.g. in qemu
317
 
        return self.config.getWithDefault("NonInteractive","RealReboot", False)
318
 
    def error(self, summary, msg, extended_msg=None):
319
 
        " display a error "
320
 
        logging.error("%s %s (%s)" % (summary, msg, extended_msg))
321
 
    def abort(self):
322
 
        logging.error("view.abort called")
323
 
 
324
 
 
325
 
if __name__ == "__main__":
326
 
 
327
 
  view = DistUpgradeViewNonInteractive()
328
 
  ap = NonInteractiveAcquireProgress()
329
 
  ip = NonInteractiveInstallProgress()
330
 
 
331
 
  #ip.error("linux-image-2.6.17-10-generic","post-installation script failed")
332
 
  ip.error("xserver-xorg","pre-installation script failed")
333
 
 
334
 
  cache = apt.Cache()
335
 
  for pkg in sys.argv[1:]:
336
 
    #if cache[pkg].is_installed:
337
 
    #  cache[pkg].mark_delete()
338
 
    #else:
339
 
    cache[pkg].mark_install()
340
 
  cache.commit(ap, ip)
341
 
  time.sleep(2)
342
 
  sys.exit(0)