~ubuntu-core-dev/update-manager/main

« back to all changes in this revision

Viewing changes to DistUpgrade/DistUpgradeViewNonInteractive.py

merge from move-changelogs branch

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