src/checkforupdates.py
changeset 2991 75e616731cc3
parent 2990 2cc6693a7d83
child 2992 e48a94cff862
equal deleted inserted replaced
2990:2cc6693a7d83 2991:75e616731cc3
     1 #!/usr/bin/python2.6
       
     2 #
       
     3 # CDDL HEADER START
       
     4 #
       
     5 # The contents of this file are subject to the terms of the
       
     6 # Common Development and Distribution License (the "License").
       
     7 # You may not use this file except in compliance with the License.
       
     8 #
       
     9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
       
    10 # or http://www.opensolaris.org/os/licensing.
       
    11 # See the License for the specific language governing permissions
       
    12 # and limitations under the License.
       
    13 #
       
    14 # When distributing Covered Code, include this CDDL HEADER in each
       
    15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
       
    16 # If applicable, add the following below this CDDL HEADER, with the
       
    17 # fields enclosed by brackets "[]" replaced with your own identifying
       
    18 # information: Portions Copyright [yyyy] [name of copyright owner]
       
    19 #
       
    20 # CDDL HEADER END
       
    21 #
       
    22 # Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
       
    23 #
       
    24 
       
    25 """This utility checks to see if there are any available updates for
       
    26 the relevant image.  If so, it stashes information about the updates in
       
    27 the gui cache file, for retrieval by other desktop utilities.  See also
       
    28 the update-refresh cron job."""
       
    29 
       
    30 import errno
       
    31 import getopt
       
    32 import gettext
       
    33 import locale
       
    34 import logging
       
    35 import os
       
    36 import sys
       
    37 import traceback
       
    38 import warnings
       
    39 
       
    40 import pkg.client.api as api
       
    41 import pkg.client.api_errors as apx
       
    42 import pkg.client.progress as progress
       
    43 import pkg.client.printengine as printengine
       
    44 import pkg.gui.enumerations as enumerations
       
    45 import pkg.gui.misc_non_gui as nongui_misc
       
    46 import pkg.misc as misc
       
    47 import pkg.nrlock as nrlock
       
    48 from cPickle import UnpicklingError
       
    49 from pkg.client import global_settings
       
    50 from pkg.client.pkgdefs import EXIT_OOPS
       
    51 
       
    52 logger = global_settings.logger
       
    53 
       
    54 PKG_CLIENT_NAME = "updatemanager"
       
    55 CACHE_VERSION =  3
       
    56 CACHE_NAME = ".last_refresh_cache"
       
    57 
       
    58 
       
    59 class CheckForUpdates:
       
    60         """Implements the main logic for this utility"""
       
    61 
       
    62         def __init__(self, image_directory, application_path, check_all,
       
    63             check_cache):
       
    64                 global_settings.client_name = nongui_misc.get_um_name()
       
    65                 self.api_lock = nrlock.NRLock()
       
    66                 self.image_dir_arg = image_directory
       
    67                 self.exact_match = True
       
    68                 if self.image_dir_arg == None:
       
    69                         self.image_dir_arg, self.exact_match =  \
       
    70                             api.get_default_image_root()
       
    71                 if not self.exact_match:
       
    72                         logger.debug("Unable to get image directory")
       
    73                         sys.exit(enumerations.UPDATES_UNDETERMINED)
       
    74                         
       
    75                 self.application_path = application_path
       
    76                 self.check_all = check_all
       
    77                 self.check_cache_only = check_cache
       
    78                 self.application_dir = \
       
    79                     os.environ.get("PACKAGE_MANAGER_ROOT", "/")
       
    80                 misc.setlocale(locale.LC_ALL, "")
       
    81 
       
    82                 if global_settings.verbose:
       
    83                         pe = printengine.LoggingPrintEngine(
       
    84                             logger, logging.DEBUG)
       
    85                         self.progress_tracker = \
       
    86                             progress.CommandLineProgressTracker(print_engine=pe)
       
    87                 else:
       
    88                         self.progress_tracker = progress.NullProgressTracker()
       
    89                 self.api_obj = None
       
    90                 self.return_status = enumerations.UPDATES_UNDETERMINED
       
    91                 self.pylintstub = None
       
    92 
       
    93                 # Check Updates - by default check all
       
    94                 self.api_obj = self.__get_api_obj()
       
    95                 if self.api_obj == None:
       
    96                         self.return_status = enumerations.UPDATES_UNDETERMINED
       
    97                         return
       
    98 
       
    99                 if self.check_all:
       
   100                         self.__check_for_updates()
       
   101                 elif self.check_cache_only:
       
   102                         self.__check_for_updates_cache_only()
       
   103 
       
   104         def __get_api_obj(self):
       
   105                 """Returns a singleton api instance."""
       
   106                 if self.api_obj == None:
       
   107                         api_obj = nongui_misc.get_api_object(self.image_dir_arg,
       
   108                             self.progress_tracker)
       
   109                 return api_obj
       
   110 
       
   111         def __check_for_updates_cache_only(self):
       
   112                 """Reports on the cached status of available updates"""
       
   113                 assert self.api_obj
       
   114                 self.return_status = ret = self.__check_last_refresh()
       
   115                 if ret == enumerations.UPDATES_AVAILABLE:
       
   116                         logger.debug("From cache: Updates Available")
       
   117                 elif ret == enumerations.NO_UPDATES_AVAILABLE:
       
   118                         logger.debug("From cache: No Updates Available")
       
   119                 else:
       
   120                         logger.debug("From cache: Updates Undetermined")
       
   121                 return ret
       
   122 
       
   123         def __check_for_updates(self):
       
   124                 """Plans an update for the image."""
       
   125                 assert self.api_obj
       
   126                 ret = self.__check_for_updates_cache_only()
       
   127                 if ret != enumerations.UPDATES_UNDETERMINED:
       
   128                         # Definitive answer from cache.
       
   129                         return
       
   130                 logger.debug("Checking image for updates...")
       
   131                 self.return_status = enumerations.UPDATES_UNDETERMINED
       
   132                 try:
       
   133                         #
       
   134                         # Since this program is intended to primarily be a
       
   135                         # helper for the gui components, and since the gui
       
   136                         # components are currently unaware of child images,
       
   137                         # we'll limit the available update check we're about
       
   138                         # to do to just the parent image.  If we didn't do
       
   139                         # this we could end up in a situation where the parent
       
   140                         # has no available updates, but a child image does,
       
   141                         # and then the gui (which is unaware of children)
       
   142                         # would show that no updates are available to the
       
   143                         # parent.
       
   144                         #
       
   145 
       
   146                         # Unused variable; pylint: disable=W0612
       
   147                         for pd in self.api_obj.gen_plan_update(
       
   148                             refresh_catalogs=True, noexecute=True,
       
   149                             force=True, li_ignore=[]):
       
   150                                 continue
       
   151                         stuff_to_do = not self.api_obj.planned_nothingtodo()
       
   152                 except apx.CatalogRefreshException, cre:
       
   153                         res = nongui_misc.get_catalogrefresh_exception_msg(cre)
       
   154                         logger.error(res[0])
       
   155                         return
       
   156                 except apx.ApiException, e:
       
   157                         logger.error(str(e))
       
   158                         return
       
   159 
       
   160                 self.__dump_updates_available(stuff_to_do)
       
   161                 if stuff_to_do:
       
   162                         logger.debug("From image: Updates Available")
       
   163                         self.return_status = enumerations.UPDATES_AVAILABLE
       
   164                 else:
       
   165                         logger.debug("From image: No Updates Available")
       
   166                         self.return_status = enumerations.NO_UPDATES_AVAILABLE
       
   167 
       
   168         def __check_last_refresh(self):
       
   169                 """Reads the cache if possible; if it isn't stale or corrupt
       
   170                 or out of date, return whether updates are available.
       
   171                 Otherwise return 'undetermined'."""
       
   172 
       
   173                 cache_dir = nongui_misc.get_cache_dir(self.api_obj)
       
   174                 if not cache_dir:
       
   175                         return enumerations.UPDATES_UNDETERMINED
       
   176                 try:
       
   177                         info = nongui_misc.read_cache_file(os.path.join(
       
   178                             cache_dir, CACHE_NAME + '.cpl'))
       
   179                         if len(info) == 0:
       
   180                                 logger.debug("No cache")
       
   181                                 return enumerations.UPDATES_UNDETERMINED
       
   182                         # Non-portable API used; pylint: disable=E0901
       
   183                         utsname = os.uname()
       
   184                         # pylint: disable=E1103
       
   185                         if info.get("version") != CACHE_VERSION:
       
   186                                 logger.debug("Cache version mismatch: %s" %
       
   187                                     (info.get("version") + " " + CACHE_VERSION))
       
   188                                 return enumerations.UPDATES_UNDETERMINED
       
   189                         if info.get("os_release") != utsname[2]:
       
   190                                 logger.debug("OS release mismatch: %s" %
       
   191                                     (info.get("os_release") + " " + utsname[2]))
       
   192                                 return enumerations.UPDATES_UNDETERMINED
       
   193                         if info.get("os_version") != utsname[3]:
       
   194                                 logger.debug("OS version mismatch: %s" %
       
   195                                     (info.get("os_version") + " " + utsname[3]))
       
   196                                 return enumerations.UPDATES_UNDETERMINED
       
   197                         old_publishers = info.get("publishers")
       
   198                         count = 0
       
   199                         for p in self.api_obj.get_publishers():
       
   200                                 if p.disabled:
       
   201                                         continue
       
   202                                 if old_publishers.get(p.prefix, -1) != \
       
   203                                     p.last_refreshed:
       
   204                                         return enumerations.UPDATES_UNDETERMINED
       
   205                                 count += 1
       
   206 
       
   207                         if count != len(old_publishers):
       
   208                                 return enumerations.UPDATES_UNDETERMINED
       
   209 
       
   210                         n_updates = n_installs = n_removes = 0
       
   211                         if info.get("updates_available"):
       
   212                                 n_updates = info.get("updates")
       
   213                                 n_installs = info.get("installs")
       
   214                                 n_removes = info.get("removes")
       
   215                         # pylint: enable=E1103
       
   216                         if self.check_cache_only:
       
   217                                 print "n_updates: %d" % n_updates
       
   218                                 print "n_installs: %d" % n_installs
       
   219                                 print "n_removes: %d" % n_removes
       
   220                         if (n_updates + n_installs + n_removes) > 0:
       
   221                                 return enumerations.UPDATES_AVAILABLE
       
   222                         else:
       
   223                                 return enumerations.NO_UPDATES_AVAILABLE
       
   224 
       
   225                 except (UnpicklingError, IOError):
       
   226                         return enumerations.UPDATES_UNDETERMINED
       
   227 
       
   228         def __dump_updates_available(self, stuff_to_do):
       
   229                 """Record update information to the cache file."""
       
   230                 cache_dir = nongui_misc.get_cache_dir(self.api_obj)
       
   231                 if not cache_dir:
       
   232                         return
       
   233                 publisher_list = {}
       
   234                 for p in self.api_obj.get_publishers():
       
   235                         if p.disabled:
       
   236                                 continue
       
   237                         publisher_list[p.prefix] = p.last_refreshed
       
   238                 n_installs = 0
       
   239                 n_removes = 0
       
   240                 n_updates = 0
       
   241                 plan_desc = self.api_obj.describe()
       
   242                 if plan_desc:
       
   243                         plan = plan_desc.get_changes()
       
   244                         for (orig, dest) in plan:
       
   245                                 if orig and dest:
       
   246                                         n_updates += 1
       
   247                                 elif not orig and dest:
       
   248                                         n_installs += 1
       
   249                                 elif orig and not dest:
       
   250                                         n_removes += 1
       
   251                 dump_info = {}
       
   252                 dump_info["version"] = CACHE_VERSION
       
   253                 # Non-portable API used; pylint: disable=E0901
       
   254                 dump_info["os_release"] = os.uname()[2]
       
   255                 dump_info["os_version"] = os.uname()[3]
       
   256                 dump_info["updates_available"] = stuff_to_do
       
   257                 dump_info["publishers"] = publisher_list
       
   258                 dump_info["updates"] = n_updates
       
   259                 dump_info["installs"] = n_installs
       
   260                 dump_info["removes"] = n_removes
       
   261 
       
   262                 try:
       
   263                         nongui_misc.dump_cache_file(os.path.join(
       
   264                             cache_dir, CACHE_NAME + '.cpl'), dump_info)
       
   265                 except IOError, e:
       
   266                         logger.error("Failed to dump cache: %s" % e)
       
   267                 return
       
   268 
       
   269 
       
   270 def main_func():
       
   271         """Main routine for this utility"""
       
   272         set_check_all = True
       
   273         set_check_cache = False
       
   274         image_dir = None 
       
   275         try:
       
   276                 # Unused variable pargs; pylint: disable=W0612
       
   277                 opts, pargs = getopt.getopt(sys.argv[1:], "hdnacR:",
       
   278                     ["help", "debug", "nice", "checkupdates-cache",
       
   279                     "image-dir="])
       
   280         except getopt.GetoptError, oex:
       
   281                 print >> sys.stderr, \
       
   282                     ("Usage: illegal option -- %s, for help use -h or --help" %
       
   283                     oex.opt )
       
   284                 sys.exit(enumerations.UPDATES_UNDETERMINED)
       
   285         for opt, arg in opts:
       
   286                 if opt in ("-h", "--help"):
       
   287                         print >> sys.stderr, """\n\
       
   288 Use -h (--help) to print out help.
       
   289 Use -d (--debug) to run in debug mode.
       
   290 Use -n (--nice) to run at nice level 20.
       
   291 Use -c (--checkupdates-cache) to check for updates from cache only (output results to stdout).
       
   292 Use -R (--image-dir) to specify image directory (defaults to '/')"""
       
   293                         sys.exit(0)
       
   294                 elif opt in ( "-n", "--nice"):
       
   295                         # Non-portable API used; pylint: disable=E0901
       
   296                         os.nice(20)
       
   297                 elif opt in ("-d", "--debug"):
       
   298                         global_settings.verbose = True
       
   299                 elif opt in ( "-c", "--checkupdates-cache"):
       
   300                         set_check_cache = True
       
   301                         set_check_all = False
       
   302                 elif opt in ("-R", "--image-dir"):
       
   303                         image_dir = arg
       
   304 
       
   305         if os.path.isabs(sys.argv[0]):
       
   306                 app_path = sys.argv[0]
       
   307         else:
       
   308                 cmd = os.path.join(os.getcwd(), sys.argv[0])
       
   309                 app_path = os.path.realpath(cmd)
       
   310 
       
   311         checkforupdates = CheckForUpdates(image_dir, app_path,
       
   312             set_check_all, set_check_cache)
       
   313 
       
   314         return checkforupdates.return_status
       
   315 
       
   316 #
       
   317 # Establish a specific exit status which means: "python barfed an exception"
       
   318 # so that we can more easily detect these in testing of the CLI commands.
       
   319 #
       
   320 def handle_errors(func, *args, **kwargs):
       
   321         """Catch exceptions raised by the main program function and then print
       
   322         a message and/or exit with an appropriate return code.
       
   323         """
       
   324 
       
   325         traceback_str = misc.get_traceback_message()
       
   326 
       
   327         try:
       
   328                 # Out of memory errors can be raised as EnvironmentErrors with
       
   329                 # an errno of ENOMEM, so in order to handle those exceptions
       
   330                 # with other errnos, we nest this try block and have the outer
       
   331                 # one handle the other instances.
       
   332                 try:
       
   333                         __ret = func(*args, **kwargs)
       
   334                 except (MemoryError, EnvironmentError), __e:
       
   335                         if isinstance(__e, EnvironmentError) and \
       
   336                             __e.errno != errno.ENOMEM:
       
   337                                 raise
       
   338                         logger.error("\n" + misc.out_of_memory())
       
   339                         __ret = EXIT_OOPS
       
   340         except SystemExit, __e:
       
   341                 raise __e
       
   342         except (IOError, misc.PipeError, KeyboardInterrupt), __e:
       
   343                 # Don't display any messages here to prevent possible further
       
   344                 # broken pipe (EPIPE) errors.
       
   345                 if isinstance(__e, IOError) and __e.errno != errno.EPIPE:
       
   346                         logger.error(str(__e))
       
   347                 __ret = EXIT_OOPS
       
   348         except apx.VersionException, __e:
       
   349                 logger.error("The pmcheckforupdates command appears out of "
       
   350                     "sync with the libraries provided\nby pkg:/package/pkg. "
       
   351                     "The client version is %(client)s while the library\n"
       
   352                     "API version is %(api)s." % \
       
   353                     {'client': __e.received_version,
       
   354                      'api': __e.expected_version})
       
   355                 __ret = EXIT_OOPS
       
   356         except:
       
   357                 traceback.print_exc()
       
   358                 logger.error(traceback_str)
       
   359                 __ret = 99
       
   360         return __ret
       
   361 
       
   362 
       
   363 if __name__ == "__main__":
       
   364         misc.setlocale(locale.LC_ALL, "")
       
   365         gettext.install("pkg", "/usr/share/locale",
       
   366             codeset=locale.getpreferredencoding())
       
   367 
       
   368         # Make all warnings be errors.
       
   369         warnings.simplefilter('error')
       
   370 
       
   371         __retval = handle_errors(main_func)
       
   372         try:
       
   373                 logging.shutdown()
       
   374         except IOError:
       
   375                 # Ignore python's spurious pipe problems.
       
   376                 pass
       
   377         sys.exit(__retval)