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) |
|