1
2 '''
3 # DESCRIPTION:
4 # Entropy Object Oriented Interface
5
6 Copyright (C) 2007-2009 Fabio Erculiani
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 '''
22
23 from __future__ import with_statement
24 import os
25 import sys
26 from entropy.core import Singleton
27 from entropy.output import TextInterface, bold, red, darkred, blue
28 from entropy.db import dbapi2
29 from entropy.client.interfaces.loaders import LoadersMixin
30 from entropy.client.interfaces.cache import CacheMixin
31 from entropy.client.interfaces.dep import CalculatorsMixin
32 from entropy.client.interfaces.methods import RepositoryMixin, MiscMixin, \
33 MatchMixin
34 from entropy.client.interfaces.fetch import FetchersMixin
35 from entropy.const import etpConst, etpCache, etpUi, const_debug_write
36 from entropy.core import SystemSettings, SystemSettingsPlugin
37 from entropy.misc import LogFile
38 from entropy.exceptions import SystemDatabaseError, RepositoryError
39 from entropy.i18n import _
40
42
43 import entropy.tools as entropyTools
44
45 - def __init__(self, plugin_id, helper_interface):
46 SystemSettingsPlugin.__init__(self, plugin_id, helper_interface)
47 self.__repos_files = {}
48 self.__repos_mtime = {}
49
50
51 self.__hooks_on_init = True
52
54 """
55 This function collects available repositories configuration files
56 by filling internal dict() __repos_files and __repos_mtime.
57
58 @param system_settings: SystemSettings instance
59 @type system_settings: instance of SystemSettings
60 @return: None
61 @rtype: None
62 """
63
64 self.__repos_mtime = {
65 'repos_license_whitelist': {},
66 'repos_mask': {},
67 'repos_system_mask': {},
68 'repos_critical_updates': {},
69 }
70 self.__repos_files = {
71 'repos_license_whitelist': {},
72 'repos_mask': {},
73 'repos_system_mask': {},
74 'conflicting_tagged_packages': {},
75 'repos_critical_updates': {},
76 }
77
78 dmp_dir = etpConst['dumpstoragedir']
79 for repoid in system_settings['repositories']['order']:
80
81 repos_mask_setting = {}
82 repos_mask_mtime = {}
83 repos_lic_wl_setting = {}
84 repos_lic_wl_mtime = {}
85 repo_data = system_settings['repositories']['available'][repoid]
86 repos_sm_mask_setting = {}
87 repos_sm_mask_mtime = {}
88 confl_tagged = {}
89 repos_critical_updates_setting = {}
90 repos_critical_updates_mtime = {}
91
92 maskpath = os.path.join(repo_data['dbpath'],
93 etpConst['etpdatabasemaskfile'])
94 wlpath = os.path.join(repo_data['dbpath'],
95 etpConst['etpdatabaselicwhitelistfile'])
96 sm_path = os.path.join(repo_data['dbpath'],
97 etpConst['etpdatabasesytemmaskfile'])
98 ct_path = os.path.join(repo_data['dbpath'],
99 etpConst['etpdatabaseconflictingtaggedfile'])
100 critical_path = os.path.join(repo_data['dbpath'],
101 etpConst['etpdatabasecriticalfile'])
102
103 if os.access(maskpath, os.R_OK | os.F_OK):
104 repos_mask_setting[repoid] = maskpath
105 repos_mask_mtime[repoid] = dmp_dir + "/repo_" + \
106 repoid + "_" + etpConst['etpdatabasemaskfile'] + ".mtime"
107
108 if os.access(wlpath, os.R_OK | os.F_OK):
109 repos_lic_wl_setting[repoid] = wlpath
110 repos_lic_wl_mtime[repoid] = dmp_dir + "/repo_" + \
111 repoid + "_" + etpConst['etpdatabaselicwhitelistfile'] + \
112 ".mtime"
113
114 if os.access(sm_path, os.R_OK | os.F_OK):
115 repos_sm_mask_setting[repoid] = sm_path
116 repos_sm_mask_mtime[repoid] = dmp_dir + "/repo_" + \
117 repoid + "_" + etpConst['etpdatabasesytemmaskfile'] + \
118 ".mtime"
119 if os.access(ct_path, os.R_OK | os.F_OK):
120 confl_tagged[repoid] = ct_path
121
122 if os.access(critical_path, os.R_OK | os.F_OK):
123 repos_critical_updates_setting[repoid] = critical_path
124 repos_critical_updates_mtime[repoid] = dmp_dir + "/repo_" + \
125 repoid + "_" + etpConst['etpdatabasecriticalfile'] + \
126 ".mtime"
127
128 self.__repos_files['repos_mask'].update(repos_mask_setting)
129 self.__repos_mtime['repos_mask'].update(repos_mask_mtime)
130
131 self.__repos_files['repos_license_whitelist'].update(
132 repos_lic_wl_setting)
133 self.__repos_mtime['repos_license_whitelist'].update(
134 repos_lic_wl_mtime)
135
136 self.__repos_files['repos_system_mask'].update(
137 repos_sm_mask_setting)
138 self.__repos_mtime['repos_system_mask'].update(
139 repos_sm_mask_mtime)
140
141 self.__repos_files['conflicting_tagged_packages'].update(
142 confl_tagged)
143
144 self.__repos_files['repos_critical_updates'].update(
145 repos_critical_updates_setting)
146 self.__repos_mtime['repos_critical_updates'].update(
147 repos_critical_updates_mtime)
148
149 - def __run_post_branch_migration_hooks(self, sys_settings_instance):
150
151
152 if os.getuid() != 0:
153 return
154
155 old_branch_path = etpConst['etp_previous_branch_file']
156 current_branch = sys_settings_instance['repositories']['branch']
157
158 def write_current_branch(branch):
159 old_brf = open(old_branch_path, "w")
160 old_brf.write(current_branch+"\n")
161 old_brf.flush()
162 old_brf.close()
163
164 if not os.access(old_branch_path, os.F_OK | os.R_OK):
165 write_current_branch(current_branch)
166 return
167
168 old_f = open(old_branch_path, "r")
169 old_branch = old_f.readline().strip()
170 old_f.close()
171
172 if old_branch == current_branch:
173 return
174
175 repos, err = self._helper.run_repositories_post_branch_switch_hooks(
176 old_branch, current_branch)
177 if not err:
178 write_current_branch(current_branch)
179
180 - def __run_post_branch_upgrade_hooks(self, sys_settings_instance):
181
182
183 if os.getuid() != 0:
184 return
185
186
187
188
189 try:
190 update, remove, fine, spm_fine = self._helper.calculate_world_updates(
191 critical_updates = False)
192 except (ValueError, SystemDatabaseError,):
193 update = 1
194
195
196
197 if not update:
198 self._helper.run_repository_post_branch_upgrade_hooks()
199
201
202 parser_data = {}
203
204 mask_installed = []
205 mask_installed_keys = {}
206 while (self._helper.clientDbconn != None):
207 try:
208 self._helper.clientDbconn.validateDatabase()
209 except SystemDatabaseError:
210 break
211 mc_cache = set()
212 repos_mask_list = self.__repositories_system_mask(
213 system_settings_instance)
214 m_list = repos_mask_list + system_settings_instance['system_mask']
215 for atom in m_list:
216 m_ids, m_r = self._helper.clientDbconn.atomMatch(atom,
217 multiMatch = True)
218 if m_r != 0:
219 continue
220 mykey = self.entropyTools.dep_getkey(atom)
221 if mykey not in mask_installed_keys:
222 mask_installed_keys[mykey] = set()
223 for m_id in m_ids:
224 if m_id in mc_cache:
225 continue
226 mc_cache.add(m_id)
227 mask_installed.append(m_id)
228 mask_installed_keys[mykey].add(m_id)
229 break
230
231 parser_data.update({
232 'repos_installed': mask_installed,
233 'repos_installed_keys': mask_installed_keys,
234 })
235 return parser_data
236
238 data = {
239 'cache': {},
240 }
241 return data
242
244 """
245 Parser returning system packages mask metadata read from
246 packages.db.system_mask file inside the repository directory.
247 This file contains packages that should be always kept
248 installed, extending the already defined (in repository database)
249 set of atoms.
250 """
251 system_mask = []
252 for repoid in self.__repos_files['repos_system_mask']:
253 sys_settings_instance.validate_entropy_cache(
254 self.__repos_files['repos_system_mask'][repoid],
255 self.__repos_mtime['repos_system_mask'][repoid],
256 repoid = repoid)
257 system_mask += [x for x in \
258 self.entropyTools.generic_file_content_parser(
259 self.__repos_files['repos_system_mask'][repoid]) if x \
260 not in system_mask]
261 return system_mask
262
264 """
265 Parser that generates repository settings metadata.
266
267 @param sys_settings_instance: SystemSettings instance
268 @type sys_settings_instance: instance of SystemSettings
269 @return: parsed metadata
270 @rtype: dict
271 """
272
273
274 self.__setup_repos_files(sys_settings_instance)
275
276 data = {
277 'license_whitelist': {},
278 'mask': {},
279 'system_mask': [],
280 'critical_updates': {},
281 'conflicting_tagged_packages': {},
282 }
283
284
285 """
286 Parser returning licenses considered accepted by default
287 (= GPL compatibles) read from package.lic_whitelist.
288 """
289 for repoid in self.__repos_files['repos_license_whitelist']:
290 sys_settings_instance.validate_entropy_cache(
291 self.__repos_files['repos_license_whitelist'][repoid],
292 self.__repos_mtime['repos_license_whitelist'][repoid],
293 repoid = repoid)
294
295 data['license_whitelist'][repoid] = \
296 self.entropyTools.generic_file_content_parser(
297 self.__repos_files['repos_license_whitelist'][repoid])
298
299
300 """
301 Parser returning packages masked at repository level read from
302 packages.db.mask inside the repository database directory.
303 """
304 for repoid in self.__repos_files['repos_mask']:
305 sys_settings_instance.validate_entropy_cache(
306 self.__repos_files['repos_mask'][repoid],
307 self.__repos_mtime['repos_mask'][repoid], repoid = repoid)
308
309 data['mask'][repoid] = \
310 self.entropyTools.generic_file_content_parser(
311 self.__repos_files['repos_mask'][repoid])
312
313
314 data['system_mask'] = self.__repositories_system_mask(
315 sys_settings_instance)
316
317
318 """
319 Parser returning critical packages list metadata read from
320 packages.db.critical file inside the repository directory.
321 This file contains packages that should be always updated
322 before anything else.
323 """
324 for repoid in self.__repos_files['repos_critical_updates']:
325 sys_settings_instance.validate_entropy_cache(
326 self.__repos_files['repos_critical_updates'][repoid],
327 self.__repos_mtime['repos_critical_updates'][repoid],
328 repoid = repoid)
329 data['critical_updates'][repoid] = \
330 self.entropyTools.generic_file_content_parser(
331 self.__repos_files['repos_critical_updates'][repoid])
332
333
334
335 """
336 Parser returning packages that could have been installed because
337 they aren't in the same scope, but ending up creating critical
338 issues. You can see it as a configurable conflict map.
339 """
340
341 repoids = [x for x in sys_settings_instance['repositories']['order'] \
342 if x in self.__repos_files['conflicting_tagged_packages']]
343 for repoid in repoids:
344 filepath = self.__repos_files['conflicting_tagged_packages'].get(
345 repoid)
346 if os.access(filepath, os.R_OK | os.F_OK):
347 confl_f = open(filepath,"r")
348 content = confl_f.readlines()
349 confl_f.close()
350 content = [x.strip().rsplit("#", 1)[0].strip().split() for x \
351 in content if not x.startswith("#") and x.strip()]
352 for mydata in content:
353 if len(mydata) < 2:
354 continue
355 data['conflicting_tagged_packages'][mydata[0]] = mydata[1:]
356
357 return data
358
359
361
362 """
363 Parses Entropy client system configuration file.
364
365 @return dict data
366 """
367
368 data = {
369 'filesbackup': etpConst['filesbackup'],
370 'forcedupdates': etpConst['forcedupdates'],
371 'ignore_spm_downgrades': etpConst['spm']['ignore-spm-downgrades'],
372 'collisionprotect': etpConst['collisionprotect'],
373 'configprotect': etpConst['configprotect'][:],
374 'configprotectmask': etpConst['configprotectmask'][:],
375 'configprotectskip': etpConst['configprotectskip'][:],
376 }
377
378 cli_conf = etpConst['clientconf']
379 if not (os.path.isfile(cli_conf) and os.access(cli_conf, os.R_OK)):
380 return data
381
382 client_f = open(cli_conf,"r")
383 clientconf = [x.strip() for x in client_f.readlines() if \
384 x.strip() and not x.strip().startswith("#")]
385 client_f.close()
386 for line in clientconf:
387
388 split_line = line.split("|")
389 split_line_len = len(split_line)
390
391 if line.startswith("filesbackup|") and (split_line_len == 2):
392
393 compatopt = split_line[1].strip().lower()
394 if compatopt in ("disable", "disabled","false", "0", "no",):
395 data['filesbackup'] = False
396
397 if line.startswith("forcedupdates|") and (split_line_len == 2):
398
399 compatopt = split_line[1].strip().lower()
400 if compatopt in ("disable", "disabled","false", "0", "no",):
401 data['forcedupdates'] = False
402 else:
403 data['forcedupdates'] = True
404
405 elif line.startswith("ignore-spm-downgrades|") and \
406 (split_line_len == 2):
407
408 compatopt = split_line[1].strip().lower()
409 if compatopt in ("enable", "enabled", "true", "1", "yes"):
410 data['ignore_spm_downgrades'] = True
411
412 elif line.startswith("collisionprotect|") and (split_line_len == 2):
413
414 collopt = split_line[1].strip()
415 if collopt.lower() in ("0", "1", "2",):
416 data['collisionprotect'] = int(collopt)
417
418 elif line.startswith("configprotect|") and (split_line_len == 2):
419
420 configprotect = split_line[1].strip()
421 for myprot in configprotect.split():
422 data['configprotect'].append(
423 unicode(myprot,'raw_unicode_escape'))
424
425 elif line.startswith("configprotectmask|") and \
426 (split_line_len == 2):
427
428 configprotect = split_line[1].strip()
429 for myprot in configprotect.split():
430 data['configprotectmask'].append(
431 unicode(myprot,'raw_unicode_escape'))
432
433 elif line.startswith("configprotectskip|") and \
434 (split_line_len == 2):
435
436 configprotect = split_line[1].strip()
437 for myprot in configprotect.split():
438 data['configprotectskip'].append(
439 etpConst['systemroot']+unicode(myprot,
440 'raw_unicode_escape'))
441
442 return data
443
444 - def post_setup(self, system_settings_instance):
445 """
446 Reimplemented from SystemSettingsPlugin.
447 """
448 if not self.__hooks_on_init:
449
450 self.__run_post_branch_migration_hooks(system_settings_instance)
451
452
453 self.__run_post_branch_upgrade_hooks(system_settings_instance)
454 self.__hooks_on_init = False
455
456
457 -class Client(Singleton, TextInterface, LoadersMixin, CacheMixin, CalculatorsMixin, \
458 RepositoryMixin, MiscMixin, MatchMixin, FetchersMixin):
459
460 - def init_singleton(self, indexing = True, noclientdb = 0,
461 xcache = True, user_xcache = False, repo_validation = True,
462 load_ugc = True, url_fetcher = None,
463 multiple_url_fetcher = None):
578
579
581 self.__instance_destroyed = True
582 if hasattr(self,'clientDbconn'):
583 if self.clientDbconn != None:
584 self.clientDbconn.closeDB()
585 del self.clientDbconn
586 if hasattr(self,'FileUpdates'):
587 del self.FileUpdates
588 if hasattr(self,'clientLog'):
589 self.clientLog.close()
590 if hasattr(self,'SystemSettings') and \
591 hasattr(self,'sys_settings_client_plugin_id'):
592
593 if hasattr(self.SystemSettings,'remove_plugin'):
594 try:
595 self.SystemSettings.remove_plugin(
596 self.sys_settings_client_plugin_id)
597 except KeyError:
598 pass
599
600 self.close_all_repositories(mask_clear = False)
601 self.closeAllSecurity()
602 self.closeAllQA()
603
606 """
607 Service method used to sync package names with Source Package Manager
608 via metadata stored in Repository dbs collected at server-time.
609 Source Package Manager can change package names, categories or slot
610 and Entropy repositories must be kept in sync.
611
612 In other words, it checks for /usr/portage/profiles/updates changes,
613 of course indirectly, since there is no way entropy.client can directly
614 depend on Portage.
615
616 @param repository_identifier: repository identifier which repo_db
617 parameter is bound
618 @type repository_identifier: string
619 @param repo_db: repository database instance
620 @type repo_db: entropy.db.EntropyRepository
621 @return: bool stating if changes have been made
622 @rtype: bool
623 """
624 if not self.clientDbconn:
625
626 return False
627
628 etpConst['client_treeupdatescalled'].add(repository_identifier)
629
630 doRescan = False
631 shell_rescan = os.getenv("ETP_TREEUPDATES_RESCAN")
632 if shell_rescan:
633 doRescan = True
634
635
636 stored_digest = repo_db.retrieveRepositoryUpdatesDigest(
637 repository_identifier)
638 if stored_digest == -1:
639 doRescan = True
640
641
642 client_digest = "0"
643 if not doRescan:
644 client_digest = self.clientDbconn.retrieveRepositoryUpdatesDigest(
645 repository_identifier)
646
647 if doRescan or (str(stored_digest) != str(client_digest)) or force:
648
649
650 self.clientDbconn.clearTreeupdatesEntries(repository_identifier)
651
652
653 update_actions = repo_db.retrieveTreeUpdatesActions(
654 repository_identifier)
655
656 update_actions = self.clientDbconn.filterTreeUpdatesActions(
657 update_actions)
658
659 if update_actions:
660
661 mytxt = "%s: %s." % (
662 bold(_("ATTENTION")),
663 red(_("forcing packages metadata update")),
664 )
665 self.updateProgress(
666 mytxt,
667 importance = 1,
668 type = "info",
669 header = darkred(" * ")
670 )
671 mytxt = "%s %s." % (
672 red(_("Updating system database using repository")),
673 blue(repository_identifier),
674 )
675 self.updateProgress(
676 mytxt,
677 importance = 1,
678 type = "info",
679 header = darkred(" * ")
680 )
681
682 self.clientDbconn.runTreeUpdatesActions(update_actions)
683
684
685 self.clientDbconn.setRepositoryUpdatesDigest(repository_identifier,
686 stored_digest)
687
688 self.clientDbconn.addRepositoryUpdatesActions(etpConst['clientdbid'],
689 update_actions, self.SystemSettings['repositories']['branch'])
690 self.clientDbconn.commitChanges()
691
692 self.clientDbconn.clearCache()
693 return True
694
696 return self.__instance_destroyed
697
700