Most of you know about the plugin-sync branch of our portage.git repo. I believe it is ready for review and merge into master to become the new sync code for the future.
Rather than follow this with 49 patch mails ... I produced a final git diff of all the changes. I have cleaned up and squashed the original commits to a point I do not want to squash further. This will keep enough development history for important changes for future troubleshooting if it is needed. Instead of inlining this diff like most people prefer, I have attached it. It is 4.4K+ lines long. Probably too long for many email clients. Plus opeing it in an editor will allow the editors syntax highlighting to make code review easier. I have fully rebased the code on the current master (commit 582cb806f88 QA patch) and pushed it to both our gentoo repo and our github gentoo/portage repo. For those that wish to review it using githubs interface. You have that option too. Thank you to all those that contributed to the code and testing. -- Brian Dolbec <dolsen>
diff --git a/bin/emerge-webrsync b/bin/emerge-webrsync index 2f0689c..43f92b6 100755 --- a/bin/emerge-webrsync +++ b/bin/emerge-webrsync @@ -501,8 +501,8 @@ main() { # This is a sanity check to help prevent people like funtoo users # from accidentally wiping out their git tree. - if [[ -n ${repo_sync_type} && ${repo_sync_type} != rsync ]] ; then - echo "The current sync-type attribute of repository 'gentoo' is not set to 'rsync':" >&2 + if [ [ -n ${repo_sync_type} ] && [ ${repo_sync_type} != rsync -o ${repo_sync_type} != websync ] ] ; then + echo "The current sync-type attribute of repository 'gentoo' is not set to 'rsync' or 'websync':" >&2 echo >&2 echo " sync-type=${repo_sync_type}" >&2 echo >&2 diff --git a/cnf/repos.conf b/cnf/repos.conf index 8c657da..1ca98ca 100644 --- a/cnf/repos.conf +++ b/cnf/repos.conf @@ -5,3 +5,4 @@ main-repo = gentoo location = /usr/portage sync-type = rsync sync-uri = rsync://rsync.gentoo.org/gentoo-portage +auto-sync = yes diff --git a/man/emaint.1 b/man/emaint.1 index 8356299..f02bc68 100644 --- a/man/emaint.1 +++ b/man/emaint.1 @@ -37,6 +37,9 @@ Perform package move updates for binary packages located in \fBPKGDIR\fR. .BR moveinst Perform package move updates for installed packages. .TP +.BR sync +Perform sync actions on specified repositories. +.TP .BR world Fix problems in the \fIworld\fR file. .SH DEFAULT OPTIONS @@ -46,7 +49,7 @@ Check for any problems that may exist. (all commands) .TP .B \-f, \-\-fix Fix any problems that may exist. (not all commands) -.SH OPTIONS +.SH OPTIONS logs command .TP .B \-C, \-\-clean Cleans the logs from \fBPORT_LOGDIR\fR (logs command only) @@ -58,6 +61,16 @@ OPTION (logs command only) .B \-t NUM, \-\-time NUM Changes the minimum age \fBNUM\fR (in days) of the logs to be listed or deleted. (logs command only) +.SH OPTIONS sync command +.TP +.B \-a, \-\-auto +Sync repositories which have its auto\-sync setting set yes, true. (sync command only) +.TP +.B \-A, \-\-allrepos +Sync all repositories which have a sync\-uri specified. (sync command only) +.TP +.B \-r, \-\-repo REPO +Sync the repository specified. (sync command only) .SH "REPORTING BUGS" Please report bugs via http://bugs.gentoo.org/ .SH AUTHORS diff --git a/man/emerge.1 b/man/emerge.1 index e32bb43..f197984 100644 --- a/man/emerge.1 +++ b/man/emerge.1 @@ -240,7 +240,7 @@ the package name. \fBTake caution\fR as the descriptions are also matched as regular expressions. .TP .BR \-\-sync -Updates repositories, for which sync\-type and sync\-uri attributes are +Updates repositories, for which auto\-sync, sync\-type and sync\-uri attributes are set in repos.conf. See \fBportage\fR(5) for more information. The \fBPORTAGE_SYNC_STALE\fR variable configures warnings that are shown when emerge \-\-sync has not @@ -251,6 +251,12 @@ The emerge \-\-sync action will revert local changes (e.g. modifications or additions of files) inside repositories synchronized using rsync. \fBNOTE:\fR +The emerge \-\-sync command is a compatibility command. Sync operations are +now performed using the the new emaint sync module. This new emaint sync module +has greater functionality and flexibility. Please refer to \fBemaint(1)\fR for +more information about sync operations. + +\fBNOTE:\fR The \fBemerge\-webrsync\fR program will download the entire portage tree as a tarball, which is much faster than emerge \-\-sync for first time syncs. diff --git a/man/portage.5 b/man/portage.5 index e399f0f..efafc27 100644 --- a/man/portage.5 +++ b/man/portage.5 @@ -818,6 +818,16 @@ When 'force = aliases' attribute is not set, \fBegencache\fR(1), since operations performed by these tools are inherently \fBnot\fR \fIsite\-specific\fR. .TP +.B auto\-sync +This setting determines if the repo will be synced during "\fBemerge \-\-sync\fR" or +"\fBemaint sync \-\-auto\fR" runs. This allows for repositories to be synced only when +desired via "\fBemaint sync \-\-repo foo\fR". +.br +Valid values: yes, no, true, false. +.br +If unset, the repo will be treated as set +no, false. +.TP .B eclass\-overrides Makes given repository inherit eclasses from specified repositories. .br diff --git a/pym/_emerge/actions.py b/pym/_emerge/actions.py index fa04e10..bd068a7 100644 --- a/pym/_emerge/actions.py +++ b/pym/_emerge/actions.py @@ -27,13 +27,13 @@ portage.proxy.lazyimport.lazyimport(globals(), 'portage.debug', 'portage.news:count_unread_news,display_news_notifications', 'portage.util._get_vm_info:get_vm_info', + 'portage.emaint.modules.sync.sync:SyncRepos', '_emerge.chk_updated_cfg_files:chk_updated_cfg_files', '_emerge.help:help@emerge_help', '_emerge.post_emerge:display_news_notification,post_emerge', '_emerge.stdout_spinner:stdout_spinner', ) -from portage.localization import _ from portage import os from portage import shutil from portage import eapi_is_supported, _encodings, _unicode_decode @@ -45,7 +45,7 @@ from portage.dbapi._expand_new_virt import expand_new_virt from portage.dep import Atom from portage.eclass_cache import hashed_path from portage.exception import InvalidAtom, InvalidData, ParseError -from portage.output import blue, bold, colorize, create_color_func, darkgreen, \ +from portage.output import blue, colorize, create_color_func, darkgreen, \ red, xtermTitle, xtermTitleReset, yellow good = create_color_func("GOOD") bad = create_color_func("BAD") @@ -62,6 +62,8 @@ from portage.util._async.run_main_scheduler import run_main_scheduler from portage.util._async.SchedulerInterface import SchedulerInterface from portage.util._eventloop.global_event_loop import global_event_loop from portage._global_updates import _global_updates +from portage.sync.old_tree_timestamp import old_tree_timestamp_warn +from portage.metadata import action_metadata from _emerge.clear_caches import clear_caches from _emerge.countdown import countdown @@ -79,8 +81,6 @@ from _emerge.Scheduler import Scheduler from _emerge.search import search from _emerge.SetArg import SetArg from _emerge.show_invalid_depstring_notice import show_invalid_depstring_notice -from _emerge.sync.getaddrinfo_validate import getaddrinfo_validate -from _emerge.sync.old_tree_timestamp import old_tree_timestamp_warn from _emerge.unmerge import unmerge from _emerge.UnmergeDepPriority import UnmergeDepPriority from _emerge.UseFlagDisplay import pkg_use_display @@ -1798,200 +1798,6 @@ def action_info(settings, trees, myopts, myfiles): tree="bintree") shutil.rmtree(tmpdir) -def action_metadata(settings, portdb, myopts, porttrees=None): - if porttrees is None: - porttrees = portdb.porttrees - portage.writemsg_stdout("\n>>> Updating Portage cache\n") - old_umask = os.umask(0o002) - cachedir = os.path.normpath(settings.depcachedir) - if cachedir in ["/", "/bin", "/dev", "/etc", "/home", - "/lib", "/opt", "/proc", "/root", "/sbin", - "/sys", "/tmp", "/usr", "/var"]: - print("!!! PORTAGE_DEPCACHEDIR IS SET TO A PRIMARY " + \ - "ROOT DIRECTORY ON YOUR SYSTEM.", file=sys.stderr) - print("!!! This is ALMOST CERTAINLY NOT what you want: '%s'" % cachedir, file=sys.stderr) - sys.exit(73) - if not os.path.exists(cachedir): - os.makedirs(cachedir) - - auxdbkeys = portdb._known_keys - - class TreeData(object): - __slots__ = ('dest_db', 'eclass_db', 'path', 'src_db', 'valid_nodes') - def __init__(self, dest_db, eclass_db, path, src_db): - self.dest_db = dest_db - self.eclass_db = eclass_db - self.path = path - self.src_db = src_db - self.valid_nodes = set() - - porttrees_data = [] - for path in porttrees: - src_db = portdb._pregen_auxdb.get(path) - if src_db is None: - # portdbapi does not populate _pregen_auxdb - # when FEATURES=metadata-transfer is enabled - src_db = portdb._create_pregen_cache(path) - - if src_db is not None: - porttrees_data.append(TreeData(portdb.auxdb[path], - portdb.repositories.get_repo_for_location(path).eclass_db, path, src_db)) - - porttrees = [tree_data.path for tree_data in porttrees_data] - - quiet = settings.get('TERM') == 'dumb' or \ - '--quiet' in myopts or \ - not sys.stdout.isatty() - - onProgress = None - if not quiet: - progressBar = portage.output.TermProgressBar() - progressHandler = ProgressHandler() - onProgress = progressHandler.onProgress - def display(): - progressBar.set(progressHandler.curval, progressHandler.maxval) - progressHandler.display = display - def sigwinch_handler(signum, frame): - lines, progressBar.term_columns = \ - portage.output.get_term_size() - signal.signal(signal.SIGWINCH, sigwinch_handler) - - # Temporarily override portdb.porttrees so portdb.cp_all() - # will only return the relevant subset. - portdb_porttrees = portdb.porttrees - portdb.porttrees = porttrees - try: - cp_all = portdb.cp_all() - finally: - portdb.porttrees = portdb_porttrees - - curval = 0 - maxval = len(cp_all) - if onProgress is not None: - onProgress(maxval, curval) - - # TODO: Display error messages, but do not interfere with the progress bar. - # Here's how: - # 1) erase the progress bar - # 2) show the error message - # 3) redraw the progress bar on a new line - - for cp in cp_all: - for tree_data in porttrees_data: - - src_chf = tree_data.src_db.validation_chf - dest_chf = tree_data.dest_db.validation_chf - dest_chf_key = '_%s_' % dest_chf - dest_chf_getter = operator.attrgetter(dest_chf) - - for cpv in portdb.cp_list(cp, mytree=tree_data.path): - tree_data.valid_nodes.add(cpv) - try: - src = tree_data.src_db[cpv] - except (CacheError, KeyError): - continue - - ebuild_location = portdb.findname(cpv, mytree=tree_data.path) - if ebuild_location is None: - continue - ebuild_hash = hashed_path(ebuild_location) - - try: - if not tree_data.src_db.validate_entry(src, - ebuild_hash, tree_data.eclass_db): - continue - except CacheError: - continue - - eapi = src.get('EAPI') - if not eapi: - eapi = '0' - eapi_supported = eapi_is_supported(eapi) - if not eapi_supported: - continue - - dest = None - try: - dest = tree_data.dest_db[cpv] - except (KeyError, CacheError): - pass - - for d in (src, dest): - if d is not None and d.get('EAPI') in ('', '0'): - del d['EAPI'] - - if src_chf != 'mtime': - # src may contain an irrelevant _mtime_ which corresponds - # to the time that the cache entry was written - src.pop('_mtime_', None) - - if src_chf != dest_chf: - # populate src entry with dest_chf_key - # (the validity of the dest_chf that we generate from the - # ebuild here relies on the fact that we already used - # validate_entry to validate the ebuild with src_chf) - src[dest_chf_key] = dest_chf_getter(ebuild_hash) - - if dest is not None: - if not (dest[dest_chf_key] == src[dest_chf_key] and \ - tree_data.eclass_db.validate_and_rewrite_cache( - dest['_eclasses_'], tree_data.dest_db.validation_chf, - tree_data.dest_db.store_eclass_paths) is not None and \ - set(dest['_eclasses_']) == set(src['_eclasses_'])): - dest = None - else: - # We don't want to skip the write unless we're really - # sure that the existing cache is identical, so don't - # trust _mtime_ and _eclasses_ alone. - for k in auxdbkeys: - if dest.get(k, '') != src.get(k, ''): - dest = None - break - - if dest is not None: - # The existing data is valid and identical, - # so there's no need to overwrite it. - continue - - try: - tree_data.dest_db[cpv] = src - except CacheError: - # ignore it; can't do anything about it. - pass - - curval += 1 - if onProgress is not None: - onProgress(maxval, curval) - - if onProgress is not None: - onProgress(maxval, curval) - - for tree_data in porttrees_data: - try: - dead_nodes = set(tree_data.dest_db) - except CacheError as e: - writemsg_level("Error listing cache entries for " + \ - "'%s': %s, continuing...\n" % (tree_data.path, e), - level=logging.ERROR, noiselevel=-1) - del e - else: - dead_nodes.difference_update(tree_data.valid_nodes) - for cpv in dead_nodes: - try: - del tree_data.dest_db[cpv] - except (KeyError, CacheError): - pass - - if not quiet: - # make sure the final progress is displayed - progressHandler.display() - print() - signal.signal(signal.SIGWINCH, signal.SIG_DFL) - - portdb.flush_cache() - sys.stdout.flush() - os.umask(old_umask) - def action_regen(settings, portdb, max_jobs, max_load): xterm_titles = "notitles" not in settings.features emergelog(xterm_titles, " === regen") @@ -2035,668 +1841,15 @@ def action_sync(emerge_config, trees=DeprecationWarning, emerge_config = load_emerge_config( action=action, args=[], trees=trees, opts=opts) - xterm_titles = "notitles" not in \ - emerge_config.target_config.settings.features - emergelog(xterm_titles, " === sync") - - selected_repos = [] - unknown_repo_names = [] - missing_sync_type = [] - if emerge_config.args: - for repo_name in emerge_config.args: - try: - repo = emerge_config.target_config.settings.repositories[repo_name] - except KeyError: - unknown_repo_names.append(repo_name) - else: - selected_repos.append(repo) - if repo.sync_type is None: - missing_sync_type.append(repo) + syncer = SyncRepos(emerge_config) - if unknown_repo_names: - writemsg_level("!!! %s\n" % _("Unknown repo(s): %s") % - " ".join(unknown_repo_names), - level=logging.ERROR, noiselevel=-1) - - if missing_sync_type: - writemsg_level("!!! %s\n" % - _("Missing sync-type for repo(s): %s") % - " ".join(repo.name for repo in missing_sync_type), - level=logging.ERROR, noiselevel=-1) - if unknown_repo_names or missing_sync_type: - return 1 - - else: - selected_repos.extend(emerge_config.target_config.settings.repositories) - - for repo in selected_repos: - if repo.sync_type is not None: - returncode = _sync_repo(emerge_config, repo) - if returncode != os.EX_OK: - return returncode - - # Reload the whole config from scratch. - portage._sync_mode = False - load_emerge_config(emerge_config=emerge_config) - adjust_configs(emerge_config.opts, emerge_config.trees) + retvals = syncer.auto_sync(options={'return-messages': False}) - if emerge_config.opts.get('--package-moves') != 'n' and \ - _global_updates(emerge_config.trees, - emerge_config.target_config.mtimedb["updates"], - quiet=("--quiet" in emerge_config.opts)): - emerge_config.target_config.mtimedb.commit() - # Reload the whole config from scratch. - load_emerge_config(emerge_config=emerge_config) - adjust_configs(emerge_config.opts, emerge_config.trees) - - mybestpv = emerge_config.target_config.trees['porttree'].dbapi.xmatch( - "bestmatch-visible", portage.const.PORTAGE_PACKAGE_ATOM) - mypvs = portage.best( - emerge_config.target_config.trees['vartree'].dbapi.match( - portage.const.PORTAGE_PACKAGE_ATOM)) - - chk_updated_cfg_files(emerge_config.target_config.root, - portage.util.shlex_split( - emerge_config.target_config.settings.get("CONFIG_PROTECT", ""))) - - if mybestpv != mypvs and "--quiet" not in emerge_config.opts: - print() - print(warn(" * ")+bold("An update to portage is available.")+" It is _highly_ recommended") - print(warn(" * ")+"that you update portage now, before any other packages are updated.") - print() - print(warn(" * ")+"To update portage, run 'emerge --oneshot portage' now.") - print() - - display_news_notification(emerge_config.target_config, emerge_config.opts) + if retvals: + return retvals[0][1] return os.EX_OK -def _sync_repo(emerge_config, repo): - settings, trees, mtimedb = emerge_config - myopts = emerge_config.opts - enter_invalid = '--ask-enter-invalid' in myopts - xterm_titles = "notitles" not in settings.features - msg = ">>> Synchronization of repository '%s' located in '%s'..." % (repo.name, repo.location) - emergelog(xterm_titles, msg) - writemsg_level(msg + "\n") - out = portage.output.EOutput() - try: - st = os.stat(repo.location) - except OSError: - st = None - if st is None: - print(">>> '%s' not found, creating it." % repo.location) - portage.util.ensure_dirs(repo.location, mode=0o755) - st = os.stat(repo.location) - - usersync_uid = None - spawn_kwargs = {} - spawn_kwargs["env"] = settings.environ() - if 'usersync' in settings.features and \ - portage.data.secpass >= 2 and \ - (st.st_uid != os.getuid() and st.st_mode & 0o700 or \ - st.st_gid != os.getgid() and st.st_mode & 0o070): - try: - homedir = pwd.getpwuid(st.st_uid).pw_dir - except KeyError: - pass - else: - # Drop privileges when syncing, in order to match - # existing uid/gid settings. - usersync_uid = st.st_uid - spawn_kwargs["uid"] = st.st_uid - spawn_kwargs["gid"] = st.st_gid - spawn_kwargs["groups"] = [st.st_gid] - spawn_kwargs["env"]["HOME"] = homedir - umask = 0o002 - if not st.st_mode & 0o020: - umask = umask | 0o020 - spawn_kwargs["umask"] = umask - - if usersync_uid is not None: - # PORTAGE_TMPDIR is used below, so validate it and - # bail out if necessary. - rval = _check_temp_dir(settings) - if rval != os.EX_OK: - return rval - - syncuri = repo.sync_uri - - vcs_dirs = frozenset(VCS_DIRS) - vcs_dirs = vcs_dirs.intersection(os.listdir(repo.location)) - - os.umask(0o022) - dosyncuri = syncuri - updatecache_flg = False - if repo.sync_type == "git": - # Update existing git repository, and ignore the syncuri. We are - # going to trust the user and assume that the user is in the branch - # that he/she wants updated. We'll let the user manage branches with - # git directly. - if portage.process.find_binary("git") is None: - msg = ["Command not found: git", - "Type \"emerge %s\" to enable git support." % portage.const.GIT_PACKAGE_ATOM] - for l in msg: - writemsg_level("!!! %s\n" % l, - level=logging.ERROR, noiselevel=-1) - return 1 - msg = ">>> Starting git pull in %s..." % repo.location - emergelog(xterm_titles, msg ) - writemsg_level(msg + "\n") - exitcode = portage.process.spawn_bash("cd %s ; git pull" % \ - (portage._shell_quote(repo.location),), - **portage._native_kwargs(spawn_kwargs)) - if exitcode != os.EX_OK: - msg = "!!! git pull error in %s." % repo.location - emergelog(xterm_titles, msg) - writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1) - return exitcode - msg = ">>> Git pull in %s successful" % repo.location - emergelog(xterm_titles, msg) - writemsg_level(msg + "\n") - elif repo.sync_type == "rsync": - for vcs_dir in vcs_dirs: - writemsg_level(("!!! %s appears to be under revision " + \ - "control (contains %s).\n!!! Aborting rsync sync.\n") % \ - (repo.location, vcs_dir), level=logging.ERROR, noiselevel=-1) - return 1 - rsync_binary = portage.process.find_binary("rsync") - if rsync_binary is None: - print("!!! /usr/bin/rsync does not exist, so rsync support is disabled.") - print("!!! Type \"emerge %s\" to enable rsync support." % portage.const.RSYNC_PACKAGE_ATOM) - return os.EX_UNAVAILABLE - mytimeout=180 - - rsync_opts = [] - if settings["PORTAGE_RSYNC_OPTS"] == "": - portage.writemsg("PORTAGE_RSYNC_OPTS empty or unset, using hardcoded defaults\n") - rsync_opts.extend([ - "--recursive", # Recurse directories - "--links", # Consider symlinks - "--safe-links", # Ignore links outside of tree - "--perms", # Preserve permissions - "--times", # Preserive mod times - "--omit-dir-times", - "--compress", # Compress the data transmitted - "--force", # Force deletion on non-empty dirs - "--whole-file", # Don't do block transfers, only entire files - "--delete", # Delete files that aren't in the master tree - "--stats", # Show final statistics about what was transfered - "--human-readable", - "--timeout="+str(mytimeout), # IO timeout if not done in X seconds - "--exclude=/distfiles", # Exclude distfiles from consideration - "--exclude=/local", # Exclude local from consideration - "--exclude=/packages", # Exclude packages from consideration - ]) - - else: - # The below validation is not needed when using the above hardcoded - # defaults. - - portage.writemsg("Using PORTAGE_RSYNC_OPTS instead of hardcoded defaults\n", 1) - rsync_opts.extend(portage.util.shlex_split( - settings.get("PORTAGE_RSYNC_OPTS", ""))) - for opt in ("--recursive", "--times"): - if opt not in rsync_opts: - portage.writemsg(yellow("WARNING:") + " adding required option " + \ - "%s not included in PORTAGE_RSYNC_OPTS\n" % opt) - rsync_opts.append(opt) - - for exclude in ("distfiles", "local", "packages"): - opt = "--exclude=/%s" % exclude - if opt not in rsync_opts: - portage.writemsg(yellow("WARNING:") + \ - " adding required option %s not included in " % opt + \ - "PORTAGE_RSYNC_OPTS (can be overridden with --exclude='!')\n") - rsync_opts.append(opt) - - if syncuri.rstrip("/").endswith(".gentoo.org/gentoo-portage"): - def rsync_opt_startswith(opt_prefix): - for x in rsync_opts: - if x.startswith(opt_prefix): - return True - return False - - if not rsync_opt_startswith("--timeout="): - rsync_opts.append("--timeout=%d" % mytimeout) - - for opt in ("--compress", "--whole-file"): - if opt not in rsync_opts: - portage.writemsg(yellow("WARNING:") + " adding required option " + \ - "%s not included in PORTAGE_RSYNC_OPTS\n" % opt) - rsync_opts.append(opt) - - if "--quiet" in myopts: - rsync_opts.append("--quiet") # Shut up a lot - else: - rsync_opts.append("--verbose") # Print filelist - - if "--verbose" in myopts: - rsync_opts.append("--progress") # Progress meter for each file - - if "--debug" in myopts: - rsync_opts.append("--checksum") # Force checksum on all files - - # Real local timestamp file. - servertimestampfile = os.path.join( - repo.location, "metadata", "timestamp.chk") - - content = portage.util.grabfile(servertimestampfile) - mytimestamp = 0 - if content: - try: - mytimestamp = time.mktime(time.strptime(content[0], - TIMESTAMP_FORMAT)) - except (OverflowError, ValueError): - pass - del content - - try: - rsync_initial_timeout = \ - int(settings.get("PORTAGE_RSYNC_INITIAL_TIMEOUT", "15")) - except ValueError: - rsync_initial_timeout = 15 - - try: - maxretries=int(settings["PORTAGE_RSYNC_RETRIES"]) - except SystemExit as e: - raise # Needed else can't exit - except: - maxretries = -1 #default number of retries - - retries=0 - try: - proto, user_name, hostname, port = re.split( - r"(rsync|ssh)://([^:/]+@)?(\[[:\da-fA-F]*\]|[^:/]*)(:[0-9]+)?", - syncuri, maxsplit=4)[1:5] - except ValueError: - writemsg_level("!!! sync-uri is invalid: %s\n" % syncuri, - noiselevel=-1, level=logging.ERROR) - return 1 - - ssh_opts = settings.get("PORTAGE_SSH_OPTS") - - if port is None: - port="" - if user_name is None: - user_name="" - if re.match(r"^\[[:\da-fA-F]*\]$", hostname) is None: - getaddrinfo_host = hostname - else: - # getaddrinfo needs the brackets stripped - getaddrinfo_host = hostname[1:-1] - updatecache_flg=True - all_rsync_opts = set(rsync_opts) - extra_rsync_opts = portage.util.shlex_split( - settings.get("PORTAGE_RSYNC_EXTRA_OPTS","")) - all_rsync_opts.update(extra_rsync_opts) - - family = socket.AF_UNSPEC - if "-4" in all_rsync_opts or "--ipv4" in all_rsync_opts: - family = socket.AF_INET - elif socket.has_ipv6 and \ - ("-6" in all_rsync_opts or "--ipv6" in all_rsync_opts): - family = socket.AF_INET6 - - addrinfos = None - uris = [] - - try: - addrinfos = getaddrinfo_validate( - socket.getaddrinfo(getaddrinfo_host, None, - family, socket.SOCK_STREAM)) - except socket.error as e: - writemsg_level( - "!!! getaddrinfo failed for '%s': %s\n" % (hostname, - _unicode_decode(e.strerror, encoding=_encodings['stdio'])), - noiselevel=-1, level=logging.ERROR) - - if addrinfos: - - AF_INET = socket.AF_INET - AF_INET6 = None - if socket.has_ipv6: - AF_INET6 = socket.AF_INET6 - - ips_v4 = [] - ips_v6 = [] - - for addrinfo in addrinfos: - if addrinfo[0] == AF_INET: - ips_v4.append("%s" % addrinfo[4][0]) - elif AF_INET6 is not None and addrinfo[0] == AF_INET6: - # IPv6 addresses need to be enclosed in square brackets - ips_v6.append("[%s]" % addrinfo[4][0]) - - random.shuffle(ips_v4) - random.shuffle(ips_v6) - - # Give priority to the address family that - # getaddrinfo() returned first. - if AF_INET6 is not None and addrinfos and \ - addrinfos[0][0] == AF_INET6: - ips = ips_v6 + ips_v4 - else: - ips = ips_v4 + ips_v6 - - for ip in ips: - uris.append(syncuri.replace( - "//" + user_name + hostname + port + "/", - "//" + user_name + ip + port + "/", 1)) - - if not uris: - # With some configurations we need to use the plain hostname - # rather than try to resolve the ip addresses (bug #340817). - uris.append(syncuri) - - # reverse, for use with pop() - uris.reverse() - - effective_maxretries = maxretries - if effective_maxretries < 0: - effective_maxretries = len(uris) - 1 - - SERVER_OUT_OF_DATE = -1 - EXCEEDED_MAX_RETRIES = -2 - while (1): - if uris: - dosyncuri = uris.pop() - else: - writemsg("!!! Exhausted addresses for %s\n" % \ - hostname, noiselevel=-1) - return 1 - - if (retries==0): - if "--ask" in myopts: - uq = UserQuery(myopts) - if uq.query("Do you want to sync your Portage tree " + \ - "with the mirror at\n" + blue(dosyncuri) + bold("?"), - enter_invalid) == "No": - print() - print("Quitting.") - print() - sys.exit(128 + signal.SIGINT) - emergelog(xterm_titles, ">>> Starting rsync with " + dosyncuri) - if "--quiet" not in myopts: - print(">>> Starting rsync with "+dosyncuri+"...") - else: - emergelog(xterm_titles, - ">>> Starting retry %d of %d with %s" % \ - (retries, effective_maxretries, dosyncuri)) - writemsg_stdout( - "\n\n>>> Starting retry %d of %d with %s\n" % \ - (retries, effective_maxretries, dosyncuri), noiselevel=-1) - - if dosyncuri.startswith('ssh://'): - dosyncuri = dosyncuri[6:].replace('/', ':/', 1) - - if mytimestamp != 0 and "--quiet" not in myopts: - print(">>> Checking server timestamp ...") - - rsynccommand = [rsync_binary] + rsync_opts + extra_rsync_opts - - if proto == 'ssh' and ssh_opts: - rsynccommand.append("--rsh=ssh " + ssh_opts) - - if "--debug" in myopts: - print(rsynccommand) - - exitcode = os.EX_OK - servertimestamp = 0 - # Even if there's no timestamp available locally, fetch the - # timestamp anyway as an initial probe to verify that the server is - # responsive. This protects us from hanging indefinitely on a - # connection attempt to an unresponsive server which rsync's - # --timeout option does not prevent. - if True: - # Temporary file for remote server timestamp comparison. - # NOTE: If FEATURES=usersync is enabled then the tempfile - # needs to be in a directory that's readable by the usersync - # user. We assume that PORTAGE_TMPDIR will satisfy this - # requirement, since that's not necessarily true for the - # default directory used by the tempfile module. - if usersync_uid is not None: - tmpdir = settings['PORTAGE_TMPDIR'] - else: - # use default dir from tempfile module - tmpdir = None - fd, tmpservertimestampfile = \ - tempfile.mkstemp(dir=tmpdir) - os.close(fd) - if usersync_uid is not None: - portage.util.apply_permissions(tmpservertimestampfile, - uid=usersync_uid) - mycommand = rsynccommand[:] - mycommand.append(dosyncuri.rstrip("/") + \ - "/metadata/timestamp.chk") - mycommand.append(tmpservertimestampfile) - content = None - mypids = [] - try: - # Timeout here in case the server is unresponsive. The - # --timeout rsync option doesn't apply to the initial - # connection attempt. - try: - if rsync_initial_timeout: - portage.exception.AlarmSignal.register( - rsync_initial_timeout) - - mypids.extend(portage.process.spawn( - mycommand, returnpid=True, - **portage._native_kwargs(spawn_kwargs))) - exitcode = os.waitpid(mypids[0], 0)[1] - if usersync_uid is not None: - portage.util.apply_permissions(tmpservertimestampfile, - uid=os.getuid()) - content = portage.grabfile(tmpservertimestampfile) - finally: - if rsync_initial_timeout: - portage.exception.AlarmSignal.unregister() - try: - os.unlink(tmpservertimestampfile) - except OSError: - pass - except portage.exception.AlarmSignal: - # timed out - print('timed out') - # With waitpid and WNOHANG, only check the - # first element of the tuple since the second - # element may vary (bug #337465). - if mypids and os.waitpid(mypids[0], os.WNOHANG)[0] == 0: - os.kill(mypids[0], signal.SIGTERM) - os.waitpid(mypids[0], 0) - # This is the same code rsync uses for timeout. - exitcode = 30 - else: - if exitcode != os.EX_OK: - if exitcode & 0xff: - exitcode = (exitcode & 0xff) << 8 - else: - exitcode = exitcode >> 8 - - if content: - try: - servertimestamp = time.mktime(time.strptime( - content[0], TIMESTAMP_FORMAT)) - except (OverflowError, ValueError): - pass - del mycommand, mypids, content - if exitcode == os.EX_OK: - if (servertimestamp != 0) and (servertimestamp == mytimestamp): - emergelog(xterm_titles, - ">>> Cancelling sync -- Already current.") - print() - print(">>>") - print(">>> Timestamps on the server and in the local repository are the same.") - print(">>> Cancelling all further sync action. You are already up to date.") - print(">>>") - print(">>> In order to force sync, remove '%s'." % servertimestampfile) - print(">>>") - print() - return os.EX_OK - elif (servertimestamp != 0) and (servertimestamp < mytimestamp): - emergelog(xterm_titles, - ">>> Server out of date: %s" % dosyncuri) - print() - print(">>>") - print(">>> SERVER OUT OF DATE: %s" % dosyncuri) - print(">>>") - print(">>> In order to force sync, remove '%s'." % servertimestampfile) - print(">>>") - print() - exitcode = SERVER_OUT_OF_DATE - elif (servertimestamp == 0) or (servertimestamp > mytimestamp): - # actual sync - mycommand = rsynccommand + [dosyncuri+"/", repo.location] - exitcode = None - try: - exitcode = portage.process.spawn(mycommand, - **portage._native_kwargs(spawn_kwargs)) - finally: - if exitcode is None: - # interrupted - exitcode = 128 + signal.SIGINT - - # 0 Success - # 1 Syntax or usage error - # 2 Protocol incompatibility - # 5 Error starting client-server protocol - # 35 Timeout waiting for daemon connection - if exitcode not in (0, 1, 2, 5, 35): - # If the exit code is not among those listed above, - # then we may have a partial/inconsistent sync - # state, so our previously read timestamp as well - # as the corresponding file can no longer be - # trusted. - mytimestamp = 0 - try: - os.unlink(servertimestampfile) - except OSError: - pass - - if exitcode in [0,1,3,4,11,14,20,21]: - break - elif exitcode in [1,3,4,11,14,20,21]: - break - else: - # Code 2 indicates protocol incompatibility, which is expected - # for servers with protocol < 29 that don't support - # --prune-empty-directories. Retry for a server that supports - # at least rsync protocol version 29 (>=rsync-2.6.4). - pass - - retries=retries+1 - - if maxretries < 0 or retries <= maxretries: - print(">>> Retrying...") - else: - # over retries - # exit loop - updatecache_flg=False - exitcode = EXCEEDED_MAX_RETRIES - break - - if (exitcode==0): - emergelog(xterm_titles, "=== Sync completed with %s" % dosyncuri) - elif exitcode == SERVER_OUT_OF_DATE: - return 1 - elif exitcode == EXCEEDED_MAX_RETRIES: - sys.stderr.write( - ">>> Exceeded PORTAGE_RSYNC_RETRIES: %s\n" % maxretries) - return 1 - elif (exitcode>0): - msg = [] - if exitcode==1: - msg.append("Rsync has reported that there is a syntax error. Please ensure") - msg.append("that sync-uri attribute for repository '%s' is proper." % repo.name) - msg.append("sync-uri: '%s'" % repo.sync_uri) - elif exitcode==11: - msg.append("Rsync has reported that there is a File IO error. Normally") - msg.append("this means your disk is full, but can be caused by corruption") - msg.append("on the filesystem that contains repository '%s'. Please investigate" % repo.name) - msg.append("and try again after the problem has been fixed.") - msg.append("Location of repository: '%s'" % repo.location) - elif exitcode==20: - msg.append("Rsync was killed before it finished.") - else: - msg.append("Rsync has not successfully finished. It is recommended that you keep") - msg.append("trying or that you use the 'emerge-webrsync' option if you are unable") - msg.append("to use rsync due to firewall or other restrictions. This should be a") - msg.append("temporary problem unless complications exist with your network") - msg.append("(and possibly your system's filesystem) configuration.") - for line in msg: - out.eerror(line) - return exitcode - elif repo.sync_type == "cvs": - if not os.path.exists("/usr/bin/cvs"): - print("!!! /usr/bin/cvs does not exist, so CVS support is disabled.") - print("!!! Type \"emerge %s\" to enable CVS support." % portage.const.CVS_PACKAGE_ATOM) - return os.EX_UNAVAILABLE - cvs_root = syncuri - if cvs_root.startswith("cvs://"): - cvs_root = cvs_root[6:] - if not os.path.exists(os.path.join(repo.location, "CVS")): - #initial checkout - print(">>> Starting initial cvs checkout with "+syncuri+"...") - try: - os.rmdir(repo.location) - except OSError as e: - if e.errno != errno.ENOENT: - sys.stderr.write( - "!!! existing '%s' directory; exiting.\n" % repo.location) - return 1 - del e - if portage.process.spawn_bash( - "cd %s; exec cvs -z0 -d %s co -P -d %s %s" % - (portage._shell_quote(os.path.dirname(repo.location)), portage._shell_quote(cvs_root), - portage._shell_quote(os.path.basename(repo.location)), portage._shell_quote(repo.sync_cvs_repo)), - **portage._native_kwargs(spawn_kwargs)) != os.EX_OK: - print("!!! cvs checkout error; exiting.") - return 1 - else: - #cvs update - print(">>> Starting cvs update with "+syncuri+"...") - retval = portage.process.spawn_bash( - "cd %s; exec cvs -z0 -q update -dP" % \ - (portage._shell_quote(repo.location),), - **portage._native_kwargs(spawn_kwargs)) - if retval != os.EX_OK: - writemsg_level("!!! cvs update error; exiting.\n", - noiselevel=-1, level=logging.ERROR) - return retval - dosyncuri = syncuri - - # Reload the whole config from scratch. - settings, trees, mtimedb = load_emerge_config(emerge_config=emerge_config) - adjust_configs(emerge_config.opts, emerge_config.trees) - portdb = trees[settings['EROOT']]['porttree'].dbapi - - if repo.sync_type == "git": - # NOTE: Do this after reloading the config, in case - # it did not exist prior to sync, so that the config - # and portdb properly account for its existence. - exitcode = git_sync_timestamps(portdb, repo.location) - if exitcode == os.EX_OK: - updatecache_flg = True - - if updatecache_flg and "metadata-transfer" not in settings.features: - updatecache_flg = False - - if updatecache_flg and \ - os.path.exists(os.path.join(repo.location, 'metadata', 'cache')): - - # Only update cache for repo.location since that's - # the only one that's been synced here. - action_metadata(settings, portdb, myopts, porttrees=[repo.location]) - - postsync = os.path.join(settings["PORTAGE_CONFIGROOT"], portage.USER_CONFIG_PATH, "bin", "post_sync") - if os.access(postsync, os.X_OK): - retval = portage.process.spawn([postsync, dosyncuri], env=settings.environ()) - if retval != os.EX_OK: - writemsg_level(" %s spawn failed of %s\n" % (bad("*"), postsync,), - level=logging.ERROR, noiselevel=-1) - - return os.EX_OK def action_uninstall(settings, trees, ldpath_mtimes, opts, action, files, spinner): @@ -3061,145 +2214,6 @@ def getportageversion(portdir, _unused, profile, chost, vardb): return "Portage %s (%s, %s, %s, %s, %s)" % \ (portage.VERSION, pythonver, profilever, gccver, ",".join(libcver), unameout) -def git_sync_timestamps(portdb, portdir): - """ - Since git doesn't preserve timestamps, synchronize timestamps between - entries and ebuilds/eclasses. Assume the cache has the correct timestamp - for a given file as long as the file in the working tree is not modified - (relative to HEAD). - """ - - cache_db = portdb._pregen_auxdb.get(portdir) - - try: - if cache_db is None: - # portdbapi does not populate _pregen_auxdb - # when FEATURES=metadata-transfer is enabled - cache_db = portdb._create_pregen_cache(portdir) - except CacheError as e: - writemsg_level("!!! Unable to instantiate cache: %s\n" % (e,), - level=logging.ERROR, noiselevel=-1) - return 1 - - if cache_db is None: - return os.EX_OK - - if cache_db.validation_chf != 'mtime': - # newer formats like md5-dict do not require mtime sync - return os.EX_OK - - writemsg_level(">>> Synchronizing timestamps...\n") - - ec_dir = os.path.join(portdir, "eclass") - try: - ec_names = set(f[:-7] for f in os.listdir(ec_dir) \ - if f.endswith(".eclass")) - except OSError as e: - writemsg_level("!!! Unable to list eclasses: %s\n" % (e,), - level=logging.ERROR, noiselevel=-1) - return 1 - - args = [portage.const.BASH_BINARY, "-c", - "cd %s && git diff-index --name-only --diff-filter=M HEAD" % \ - portage._shell_quote(portdir)] - proc = subprocess.Popen(args, stdout=subprocess.PIPE) - modified_files = set(_unicode_decode(l).rstrip("\n") for l in proc.stdout) - rval = proc.wait() - proc.stdout.close() - if rval != os.EX_OK: - return rval - - modified_eclasses = set(ec for ec in ec_names \ - if os.path.join("eclass", ec + ".eclass") in modified_files) - - updated_ec_mtimes = {} - - for cpv in cache_db: - cpv_split = portage.catpkgsplit(cpv) - if cpv_split is None: - writemsg_level("!!! Invalid cache entry: %s\n" % (cpv,), - level=logging.ERROR, noiselevel=-1) - continue - - cat, pn, ver, rev = cpv_split - cat, pf = portage.catsplit(cpv) - relative_eb_path = os.path.join(cat, pn, pf + ".ebuild") - if relative_eb_path in modified_files: - continue - - try: - cache_entry = cache_db[cpv] - eb_mtime = cache_entry.get("_mtime_") - ec_mtimes = cache_entry.get("_eclasses_") - except KeyError: - writemsg_level("!!! Missing cache entry: %s\n" % (cpv,), - level=logging.ERROR, noiselevel=-1) - continue - except CacheError as e: - writemsg_level("!!! Unable to access cache entry: %s %s\n" % \ - (cpv, e), level=logging.ERROR, noiselevel=-1) - continue - - if eb_mtime is None: - writemsg_level("!!! Missing ebuild mtime: %s\n" % (cpv,), - level=logging.ERROR, noiselevel=-1) - continue - - try: - eb_mtime = long(eb_mtime) - except ValueError: - writemsg_level("!!! Invalid ebuild mtime: %s %s\n" % \ - (cpv, eb_mtime), level=logging.ERROR, noiselevel=-1) - continue - - if ec_mtimes is None: - writemsg_level("!!! Missing eclass mtimes: %s\n" % (cpv,), - level=logging.ERROR, noiselevel=-1) - continue - - if modified_eclasses.intersection(ec_mtimes): - continue - - missing_eclasses = set(ec_mtimes).difference(ec_names) - if missing_eclasses: - writemsg_level("!!! Non-existent eclass(es): %s %s\n" % \ - (cpv, sorted(missing_eclasses)), level=logging.ERROR, - noiselevel=-1) - continue - - eb_path = os.path.join(portdir, relative_eb_path) - try: - current_eb_mtime = os.stat(eb_path) - except OSError: - writemsg_level("!!! Missing ebuild: %s\n" % \ - (cpv,), level=logging.ERROR, noiselevel=-1) - continue - - inconsistent = False - for ec, (ec_path, ec_mtime) in ec_mtimes.items(): - updated_mtime = updated_ec_mtimes.get(ec) - if updated_mtime is not None and updated_mtime != ec_mtime: - writemsg_level("!!! Inconsistent eclass mtime: %s %s\n" % \ - (cpv, ec), level=logging.ERROR, noiselevel=-1) - inconsistent = True - break - - if inconsistent: - continue - - if current_eb_mtime != eb_mtime: - os.utime(eb_path, (eb_mtime, eb_mtime)) - - for ec, (ec_path, ec_mtime) in ec_mtimes.items(): - if ec in updated_ec_mtimes: - continue - ec_path = os.path.join(ec_dir, ec + ".eclass") - current_mtime = os.stat(ec_path)[stat.ST_MTIME] - if current_mtime != ec_mtime: - os.utime(ec_path, (ec_mtime, ec_mtime)) - updated_ec_mtimes[ec] = ec_mtime - - return os.EX_OK class _emerge_config(SlotObject): diff --git a/pym/_emerge/main.py b/pym/_emerge/main.py index 52aa9c5..7e8a800 100644 --- a/pym/_emerge/main.py +++ b/pym/_emerge/main.py @@ -1033,11 +1033,13 @@ def emerge_main(args=None): elif myaction == "moo": print(COWSAY_MOO % platform.system()) return os.EX_OK + elif myaction == "sync": + # need to set this to True now in order for the repository config + # loading to allow new repos with non-existent directories + portage._sync_mode = True # Portage needs to ensure a sane umask for the files it creates. os.umask(0o22) - if myaction == "sync": - portage._sync_mode = True emerge_config = load_emerge_config( action=myaction, args=myfiles, opts=myopts) rval = profile_check(emerge_config.trees, emerge_config.action) diff --git a/pym/_emerge/sync/__init__.py b/pym/_emerge/sync/__init__.py deleted file mode 100644 index 21a391a..0000000 --- a/pym/_emerge/sync/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright 2010 Gentoo Foundation -# Distributed under the terms of the GNU General Public License v2 diff --git a/pym/_emerge/sync/getaddrinfo_validate.py b/pym/_emerge/sync/getaddrinfo_validate.py deleted file mode 100644 index 5e6009c..0000000 --- a/pym/_emerge/sync/getaddrinfo_validate.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2010 Gentoo Foundation -# Distributed under the terms of the GNU General Public License v2 - -import sys - -if sys.hexversion >= 0x3000000: - basestring = str - -def getaddrinfo_validate(addrinfos): - """ - Validate structures returned from getaddrinfo(), - since they may be corrupt, especially when python - has IPv6 support disabled (bug #340899). - """ - valid_addrinfos = [] - for addrinfo in addrinfos: - try: - if len(addrinfo) != 5: - continue - if len(addrinfo[4]) < 2: - continue - if not isinstance(addrinfo[4][0], basestring): - continue - except TypeError: - continue - - valid_addrinfos.append(addrinfo) - - return valid_addrinfos diff --git a/pym/_emerge/sync/old_tree_timestamp.py b/pym/_emerge/sync/old_tree_timestamp.py deleted file mode 100644 index aa23a27..0000000 --- a/pym/_emerge/sync/old_tree_timestamp.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2010-2014 Gentoo Foundation -# Distributed under the terms of the GNU General Public License v2 - -from __future__ import division - -import locale -import logging -import time - -from portage import os -from portage.exception import PortageException -from portage.localization import _ -from portage.output import EOutput -from portage.util import grabfile, writemsg_level - -def have_english_locale(): - lang, enc = locale.getdefaultlocale() - if lang is not None: - lang = lang.lower() - lang = lang.split('_', 1)[0] - return lang is None or lang in ('c', 'en') - -def whenago(seconds): - sec = int(seconds) - mins = 0 - days = 0 - hrs = 0 - years = 0 - out = [] - - if sec > 60: - mins = sec // 60 - sec = sec % 60 - if mins > 60: - hrs = mins // 60 - mins = mins % 60 - if hrs > 24: - days = hrs // 24 - hrs = hrs % 24 - if days > 365: - years = days // 365 - days = days % 365 - - if years: - out.append("%dy " % years) - if days: - out.append("%dd " % days) - if hrs: - out.append("%dh " % hrs) - if mins: - out.append("%dm " % mins) - if sec: - out.append("%ds " % sec) - - return "".join(out).strip() - -def old_tree_timestamp_warn(portdir, settings): - unixtime = time.time() - default_warnsync = 30 - - timestamp_file = os.path.join(portdir, "metadata/timestamp.x") - try: - lastsync = grabfile(timestamp_file) - except PortageException: - return False - - if not lastsync: - return False - - lastsync = lastsync[0].split() - if not lastsync: - return False - - try: - lastsync = int(lastsync[0]) - except ValueError: - return False - - var_name = 'PORTAGE_SYNC_STALE' - try: - warnsync = float(settings.get(var_name, default_warnsync)) - except ValueError: - writemsg_level("!!! %s contains non-numeric value: %s\n" % \ - (var_name, settings[var_name]), - level=logging.ERROR, noiselevel=-1) - return False - - if warnsync <= 0: - return False - - if (unixtime - 86400 * warnsync) > lastsync: - out = EOutput() - if have_english_locale(): - out.ewarn("Last emerge --sync was %s ago." % \ - whenago(unixtime - lastsync)) - else: - out.ewarn(_("Last emerge --sync was %s.") % \ - time.strftime('%c', time.localtime(lastsync))) - return True - return False diff --git a/pym/portage/emaint/main.py b/pym/portage/emaint/main.py index 646883d..fea4832 100644 --- a/pym/portage/emaint/main.py +++ b/pym/portage/emaint/main.py @@ -9,8 +9,8 @@ import textwrap import portage from portage import os -from portage.emaint.module import Modules -from portage.emaint.progress import ProgressBar +from portage.module import Modules +from portage.progress import ProgressBar from portage.emaint.defaults import DEFAULT_OPTIONS from portage.util._argparse import ArgumentParser @@ -113,7 +113,7 @@ class TaskHandler(object): for task in tasks: inst = task() show_progress = self.show_progress_bar and self.isatty - # check if the function is capable of progressbar + # check if the function is capable of progressbar # and possibly override it off if show_progress and hasattr(inst, 'can_progressbar'): show_progress = inst.can_progressbar(func) @@ -153,7 +153,13 @@ def emaint_main(myargv): # files (such as the world file) have sane permissions. os.umask(0o22) - module_controller = Modules(namepath="portage.emaint.modules") + module_path = os.path.join( + (os.path.dirname( + os.path.realpath(__file__))), "modules" + ) + module_controller = Modules( + path=module_path, + namepath="portage.emaint.modules") module_names = module_controller.module_names[:] module_names.insert(0, "all") @@ -206,9 +212,9 @@ def emaint_main(myargv): tasks = [] for m in module_names[1:]: #print("DEBUG: module: %s, functions: " % (m, str(module_controller.get_functions(m)))) - if func in module_controller.get_functions(m): + if long_action in module_controller.get_functions(m): tasks.append(module_controller.get_class(m)) - elif func in module_controller.get_functions(args[0]): + elif long_action in module_controller.get_functions(args[0]): tasks = [module_controller.get_class(args[0] )] else: portage.util.writemsg( @@ -221,5 +227,6 @@ def emaint_main(myargv): # need to pass the parser options dict to the modules # so they are available if needed. task_opts = options.__dict__ + task_opts['return-messages'] = True taskmaster = TaskHandler(callback=print_results, module_output=sys.stdout) taskmaster.run_tasks(tasks, func, status, options=task_opts) diff --git a/pym/portage/emaint/module.py b/pym/portage/emaint/module.py deleted file mode 100644 index 07a0cb7..0000000 --- a/pym/portage/emaint/module.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright 2005-2014 Gentoo Foundation -# Distributed under the terms of the GNU General Public License v2 - - -from __future__ import print_function - -from portage import os -from portage.exception import PortageException -from portage.cache.mappings import ProtectedDict - - -class InvalidModuleName(PortageException): - """An invalid or unknown module name.""" - - -class Module(object): - """Class to define and hold our plug-in module - - @type name: string - @param name: the module name - @type path: the path to the new module - """ - - def __init__(self, name, namepath): - """Some variables initialization""" - self.name = name - self._namepath = namepath - self.kids_names = [] - self.kids = {} - self.initialized = self._initialize() - - def _initialize(self): - """Initialize the plug-in module - - @rtype: boolean - """ - self.valid = False - try: - mod_name = ".".join([self._namepath, self.name]) - self._module = __import__(mod_name, [], [], ["not empty"]) - self.valid = True - except ImportError as e: - print("MODULE; failed import", mod_name, " error was:", e) - return False - self.module_spec = self._module.module_spec - for submodule in self.module_spec['provides']: - kid = self.module_spec['provides'][submodule] - kidname = kid['name'] - kid['module_name'] = '.'.join([mod_name, self.name]) - kid['is_imported'] = False - self.kids[kidname] = kid - self.kids_names.append(kidname) - return True - - def get_class(self, name): - if not name or name not in self.kids_names: - raise InvalidModuleName("Module name '%s' was invalid or not" - %name + "part of the module '%s'" %self.name) - kid = self.kids[name] - if kid['is_imported']: - module = kid['instance'] - else: - try: - module = __import__(kid['module_name'], [], [], ["not empty"]) - kid['instance'] = module - kid['is_imported'] = True - except ImportError: - raise - mod_class = getattr(module, kid['class']) - return mod_class - - -class Modules(object): - """Dynamic modules system for loading and retrieving any of the - installed emaint modules and/or provided class's - - @param path: Optional path to the "modules" directory or - defaults to the directory of this file + '/modules' - @param namepath: Optional python import path to the "modules" directory or - defaults to the directory name of this file + '.modules' - """ - - def __init__(self, path=None, namepath=None): - if path: - self._module_path = path - else: - self._module_path = os.path.join(( - os.path.dirname(os.path.realpath(__file__))), "modules") - if namepath: - self._namepath = namepath - else: - self._namepath = '.'.join(os.path.dirname( - os.path.realpath(__file__)), "modules") - self._modules = self._get_all_modules() - self.modules = ProtectedDict(self._modules) - self.module_names = sorted(self._modules) - #self.modules = {} - #for mod in self.module_names: - #self.module[mod] = LazyLoad( - - def _get_all_modules(self): - """scans the emaint modules dir for loadable modules - - @rtype: dictionary of module_plugins - """ - module_dir = self._module_path - importables = [] - names = os.listdir(module_dir) - for entry in names: - # skip any __init__ or __pycache__ files or directories - if entry.startswith('__'): - continue - try: - # test for statinfo to ensure it should a real module - # it will bail if it errors - os.lstat(os.path.join(module_dir, entry, '__init__.py')) - importables.append(entry) - except EnvironmentError: - pass - kids = {} - for entry in importables: - new_module = Module(entry, self._namepath) - for module_name in new_module.kids: - kid = new_module.kids[module_name] - kid['parent'] = new_module - kids[kid['name']] = kid - return kids - - def get_module_names(self): - """Convienence function to return the list of installed modules - available - - @rtype: list - @return: the installed module names available - """ - return self.module_names - - def get_class(self, modname): - """Retrieves a module class desired - - @type modname: string - @param modname: the module class name - """ - if modname and modname in self.module_names: - mod = self._modules[modname]['parent'].get_class(modname) - else: - raise InvalidModuleName("Module name '%s' was invalid or not" - %modname + "found") - return mod - - def get_description(self, modname): - """Retrieves the module class decription - - @type modname: string - @param modname: the module class name - @type string - @return: the modules class decription - """ - if modname and modname in self.module_names: - mod = self._modules[modname]['description'] - else: - raise InvalidModuleName("Module name '%s' was invalid or not" - %modname + "found") - return mod - - def get_functions(self, modname): - """Retrieves the module class exported function names - - @type modname: string - @param modname: the module class name - @type list - @return: the modules class exported function names - """ - if modname and modname in self.module_names: - mod = self._modules[modname]['functions'] - else: - raise InvalidModuleName("Module name '%s' was invalid or not" - %modname + "found") - return mod - - def get_func_descriptions(self, modname): - """Retrieves the module class exported functions descriptions - - @type modname: string - @param modname: the module class name - @type dictionary - @return: the modules class exported functions descriptions - """ - if modname and modname in self.module_names: - desc = self._modules[modname]['func_desc'] - else: - raise InvalidModuleName("Module name '%s' was invalid or not" - %modname + "found") - return desc diff --git a/pym/portage/emaint/modules/sync/__init__.py b/pym/portage/emaint/modules/sync/__init__.py new file mode 100644 index 0000000..4070200 --- /dev/null +++ b/pym/portage/emaint/modules/sync/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +"""Check repos.conf settings and sync repositories. +""" + + +module_spec = { + 'name': 'sync', + 'description': __doc__, + 'provides':{ + 'sync-module': { + 'name': "sync", + 'class': "SyncRepos", + 'description': __doc__, + 'functions': ['allrepos', 'auto', 'repo'], + 'func_desc': { + 'repo': { + "short": "-r", "long": "--repo", + "help": "(sync module only): -r, --repo Sync the specified repo", + 'status': "Syncing %s", + 'action': 'store', + 'func': 'repo', + }, + 'allrepos': { + "short": "-A", "long": "--allrepos", + "help": "(sync module only): -A, --allrepos Sync all repos that have a sync-url defined", + 'status': "Syncing %s", + 'action': 'store_true', + 'dest': 'allrepos', + 'func': 'all_repos', + }, + 'auto': { + "short": "-a", "long": "--auto", + "help": "(sync module only): -a, --auto Sync auto-sync enabled repos only", + 'status': "Syncing %s", + 'action': 'store_true', + 'dest': 'auto', + 'func': 'auto_sync', + }, + } + } + } + } diff --git a/pym/portage/emaint/modules/sync/sync.py b/pym/portage/emaint/modules/sync/sync.py new file mode 100644 index 0000000..ab7591d --- /dev/null +++ b/pym/portage/emaint/modules/sync/sync.py @@ -0,0 +1,238 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import logging +import os +import sys + +import portage +portage._internal_caller = True +portage._sync_mode = True +from portage.localization import _ +from portage.output import bold, create_color_func +from portage.sync import get_syncer +from portage._global_updates import _global_updates +from portage.util import writemsg_level + +import _emerge +from _emerge.emergelog import emergelog + + +portage.proxy.lazyimport.lazyimport(globals(), + '_emerge.actions:adjust_configs,load_emerge_config', + '_emerge.chk_updated_cfg_files:chk_updated_cfg_files', + '_emerge.main:parse_opts', + '_emerge.post_emerge:display_news_notification', +) + +warn = create_color_func("WARN") + +if sys.hexversion >= 0x3000000: + _basestring = str +else: + _basestring = basestring + + +class SyncRepos(object): + + short_desc = "Check repos.conf settings and/or sync repositories" + + @staticmethod + def name(): + return "sync" + + + def can_progressbar(self, func): + return False + + + def __init__(self, emerge_config=None, emerge_logging=False): + '''Class init function + + @param emerge_config: optional an emerge_config instance to use + @param emerge_logging: boolean, defaults to False + ''' + if emerge_config: + self.emerge_config = emerge_config + else: + # need a basic options instance + actions, opts, _files = parse_opts([], silent=True) + self.emerge_config = load_emerge_config( + action='sync', args=_files, trees=[], opts=opts) + if emerge_logging: + _emerge.emergelog._disable = False + self.xterm_titles = "notitles" not in \ + self.emerge_config.target_config.settings.features + emergelog(self.xterm_titles, " === sync") + + + def auto_sync(self, **kwargs): + '''Sync auto-sync enabled repos''' + options = kwargs.get('options', None) + selected = self._get_repos(True) + if options.get('return-messages', False): + return self.rmessage(self._sync(selected), 'sync') + return self._sync(selected) + + + def all_repos(self, **kwargs): + '''Sync all repos defined in repos.conf''' + selected = self._get_repos(auto_sync_only=False) + options = kwargs.get('options', None) + if options.get('return-messages', False): + return self.rmessage( + self._sync(selected), + 'sync') + return self._sync(selected) + + + def repo(self, **kwargs): + '''Sync the specified repo''' + options = kwargs.get('options', None) + if options: + repos = options.get('repo', '') + return_messages = options.get('return-messages', False) + else: + return_messages = False + if isinstance(repos, _basestring): + repos = repos.split() + available = self._get_repos(auto_sync_only=False) + selected = self._match_repos(repos, available) + if return_messages: + return self.rmessage(self._sync(selected), 'sync') + return self._sync(selected) + + + @staticmethod + def _match_repos(repos, available): + '''Internal search, matches up the repo.name in repos + + @param repos: list, of repo names to match + @param avalable: list of repo objects to search + @return: list of repo objects that match + ''' + selected = [] + for repo in available: + if repo.name in repos: + selected.append(repo) + return selected + + + def _get_repos(self, auto_sync_only=True): + selected_repos = [] + unknown_repo_names = [] + missing_sync_type = [] + if self.emerge_config.args: + for repo_name in self.emerge_config.args: + print("_get_repos(): repo_name =", repo_name) + try: + repo = self.emerge_config.target_config.settings.repositories[repo_name] + except KeyError: + unknown_repo_names.append(repo_name) + else: + selected_repos.append(repo) + if repo.sync_type is None: + missing_sync_type.append(repo) + + if unknown_repo_names: + writemsg_level("!!! %s\n" % _("Unknown repo(s): %s") % + " ".join(unknown_repo_names), + level=logging.ERROR, noiselevel=-1) + + if missing_sync_type: + writemsg_level("!!! %s\n" % + _("Missing sync-type for repo(s): %s") % + " ".join(repo.name for repo in missing_sync_type), + level=logging.ERROR, noiselevel=-1) + + if unknown_repo_names or missing_sync_type: + print("missing or unknown repos... returning") + return [] + + else: + selected_repos.extend(self.emerge_config.target_config.settings.repositories) + #print("_get_repos(), selected =", selected_repos) + if auto_sync_only: + return self._filter_auto(selected_repos) + return selected_repos + + + def _filter_auto(self, repos): + selected = [] + for repo in repos: + if repo.auto_sync in ['yes', 'true']: + selected.append(repo) + return selected + + + def _sync(self, selected_repos): + if not selected_repos: + print("_sync(), nothing to sync... returning") + return [('None', os.EX_OK)] + # Portage needs to ensure a sane umask for the files it creates. + os.umask(0o22) + + sync_manager = get_syncer(self.emerge_config.target_config.settings, emergelog) + retvals = [] + for repo in selected_repos: + print("syncing repo:", repo.name) + if repo.sync_type is not None: + returncode = sync_manager.sync(self.emerge_config, repo) + #if returncode != os.EX_OK: + retvals.append((repo.name, returncode)) + + # Reload the whole config. + portage._sync_mode = False + self._reload_config() + self._do_pkg_moves() + self._check_updates() + display_news_notification(self.emerge_config.target_config, + self.emerge_config.opts) + if retvals: + return retvals + return [('None', os.EX_OK)] + + + def _do_pkg_moves(self): + if self.emerge_config.opts.get('--package-moves') != 'n' and \ + _global_updates(self.emerge_config.trees, + self.emerge_config.target_config.mtimedb["updates"], + quiet=("--quiet" in self.emerge_config.opts)): + self.emerge_config.target_config.mtimedb.commit() + # Reload the whole config. + self._reload_config() + + + def _check_updates(self): + mybestpv = self.emerge_config.target_config.trees['porttree'].dbapi.xmatch( + "bestmatch-visible", portage.const.PORTAGE_PACKAGE_ATOM) + mypvs = portage.best( + self.emerge_config.target_config.trees['vartree'].dbapi.match( + portage.const.PORTAGE_PACKAGE_ATOM)) + + chk_updated_cfg_files(self.emerge_config.target_config.root, + portage.util.shlex_split( + self.emerge_config.target_config.settings.get("CONFIG_PROTECT", ""))) + + if mybestpv != mypvs and "--quiet" not in self.emerge_config.opts: + print() + print(warn(" * ")+bold("An update to portage is available.")+" It is _highly_ recommended") + print(warn(" * ")+"that you update portage now, before any other packages are updated.") + print() + print(warn(" * ")+"To update portage, run 'emerge --oneshot portage' now.") + print() + + + def _reload_config(self): + '''Reload the whole config from scratch.''' + load_emerge_config(emerge_config=self.emerge_config) + adjust_configs(self.emerge_config.opts, self.emerge_config.trees) + + + def rmessage(self, rvals, action): + '''Creates emaint style messages to return to the task handler''' + messages = [] + for rval in rvals: + messages.append("Action: %s for repo: %s, returned code = %s" + % (action, rval[0], rval[1])) + return messages diff --git a/pym/portage/emaint/progress.py b/pym/portage/emaint/progress.py deleted file mode 100644 index e43c2af..0000000 --- a/pym/portage/emaint/progress.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2005-2012 Gentoo Foundation -# Distributed under the terms of the GNU General Public License v2 - -import time -import signal - -import portage - - -class ProgressHandler(object): - def __init__(self): - self.reset() - - def reset(self): - self.curval = 0 - self.maxval = 0 - self.last_update = 0 - self.min_display_latency = 0.2 - - def onProgress(self, maxval, curval): - self.maxval = maxval - self.curval = curval - cur_time = time.time() - if cur_time - self.last_update >= self.min_display_latency: - self.last_update = cur_time - self.display() - - def display(self): - raise NotImplementedError(self) - - -class ProgressBar(ProgressHandler): - """Class to set up and return a Progress Bar""" - - def __init__(self, isatty, **kwargs): - self.isatty = isatty - self.kwargs = kwargs - ProgressHandler.__init__(self) - self.progressBar = None - - def start(self): - if self.isatty: - self.progressBar = portage.output.TermProgressBar(**self.kwargs) - signal.signal(signal.SIGWINCH, self.sigwinch_handler) - else: - self.onProgress = None - return self.onProgress - - def set_label(self, _label): - self.kwargs['label'] = _label - - def display(self): - self.progressBar.set(self.curval, self.maxval) - - def sigwinch_handler(self, signum, frame): - lines, self.progressBar.term_columns = \ - portage.output.get_term_size() - - def stop(self): - signal.signal(signal.SIGWINCH, signal.SIG_DFL) - diff --git a/pym/portage/metadata.py b/pym/portage/metadata.py new file mode 100644 index 0000000..d2dacc7 --- /dev/null +++ b/pym/portage/metadata.py @@ -0,0 +1,210 @@ + +from __future__ import print_function + +import sys +import signal +import logging +import operator + +import portage +from portage import os +from portage import eapi_is_supported +from portage.util import writemsg_level +from portage.cache.cache_errors import CacheError +from _emerge.ProgressHandler import ProgressHandler +from portage.eclass_cache import hashed_path + + +def action_metadata(settings, portdb, myopts, porttrees=None): + if porttrees is None: + porttrees = portdb.porttrees + portage.writemsg_stdout("\n>>> Updating Portage cache\n") + #old_umask = os.umask(0o002) + cachedir = os.path.normpath(settings.depcachedir) + if cachedir in ["/", "/bin", "/dev", "/etc", "/home", + "/lib", "/opt", "/proc", "/root", "/sbin", + "/sys", "/tmp", "/usr", "/var"]: + print("!!! PORTAGE_DEPCACHEDIR IS SET TO A PRIMARY " + \ + "ROOT DIRECTORY ON YOUR SYSTEM.", file=sys.stderr) + print("!!! This is ALMOST CERTAINLY NOT what you want: '%s'" % cachedir, file=sys.stderr) + sys.exit(73) + if not os.path.exists(cachedir): + os.makedirs(cachedir) + + auxdbkeys = portdb._known_keys + + class TreeData(object): + __slots__ = ('dest_db', 'eclass_db', 'path', 'src_db', 'valid_nodes') + def __init__(self, dest_db, eclass_db, path, src_db): + self.dest_db = dest_db + self.eclass_db = eclass_db + self.path = path + self.src_db = src_db + self.valid_nodes = set() + + porttrees_data = [] + for path in porttrees: + src_db = portdb._pregen_auxdb.get(path) + if src_db is None: + # portdbapi does not populate _pregen_auxdb + # when FEATURES=metadata-transfer is enabled + src_db = portdb._create_pregen_cache(path) + + if src_db is not None: + porttrees_data.append(TreeData(portdb.auxdb[path], + portdb.repositories.get_repo_for_location(path).eclass_db, path, src_db)) + + porttrees = [tree_data.path for tree_data in porttrees_data] + + quiet = settings.get('TERM') == 'dumb' or \ + '--quiet' in myopts or \ + not sys.stdout.isatty() + + onProgress = None + if not quiet: + progressBar = portage.output.TermProgressBar() + progressHandler = ProgressHandler() + onProgress = progressHandler.onProgress + def display(): + progressBar.set(progressHandler.curval, progressHandler.maxval) + progressHandler.display = display + def sigwinch_handler(signum, frame): + lines, progressBar.term_columns = \ + portage.output.get_term_size() + signal.signal(signal.SIGWINCH, sigwinch_handler) + + # Temporarily override portdb.porttrees so portdb.cp_all() + # will only return the relevant subset. + portdb_porttrees = portdb.porttrees + portdb.porttrees = porttrees + try: + cp_all = portdb.cp_all() + finally: + portdb.porttrees = portdb_porttrees + + curval = 0 + maxval = len(cp_all) + if onProgress is not None: + onProgress(maxval, curval) + + # TODO: Display error messages, but do not interfere with the progress bar. + # Here's how: + # 1) erase the progress bar + # 2) show the error message + # 3) redraw the progress bar on a new line + + for cp in cp_all: + for tree_data in porttrees_data: + + src_chf = tree_data.src_db.validation_chf + dest_chf = tree_data.dest_db.validation_chf + dest_chf_key = '_%s_' % dest_chf + dest_chf_getter = operator.attrgetter(dest_chf) + + for cpv in portdb.cp_list(cp, mytree=tree_data.path): + tree_data.valid_nodes.add(cpv) + try: + src = tree_data.src_db[cpv] + except (CacheError, KeyError): + continue + + ebuild_location = portdb.findname(cpv, mytree=tree_data.path) + if ebuild_location is None: + continue + ebuild_hash = hashed_path(ebuild_location) + + try: + if not tree_data.src_db.validate_entry(src, + ebuild_hash, tree_data.eclass_db): + continue + except CacheError: + continue + + eapi = src.get('EAPI') + if not eapi: + eapi = '0' + eapi_supported = eapi_is_supported(eapi) + if not eapi_supported: + continue + + dest = None + try: + dest = tree_data.dest_db[cpv] + except (KeyError, CacheError): + pass + + for d in (src, dest): + if d is not None and d.get('EAPI') in ('', '0'): + del d['EAPI'] + + if src_chf != 'mtime': + # src may contain an irrelevant _mtime_ which corresponds + # to the time that the cache entry was written + src.pop('_mtime_', None) + + if src_chf != dest_chf: + # populate src entry with dest_chf_key + # (the validity of the dest_chf that we generate from the + # ebuild here relies on the fact that we already used + # validate_entry to validate the ebuild with src_chf) + src[dest_chf_key] = dest_chf_getter(ebuild_hash) + + if dest is not None: + if not (dest[dest_chf_key] == src[dest_chf_key] and \ + tree_data.eclass_db.validate_and_rewrite_cache( + dest['_eclasses_'], tree_data.dest_db.validation_chf, + tree_data.dest_db.store_eclass_paths) is not None and \ + set(dest['_eclasses_']) == set(src['_eclasses_'])): + dest = None + else: + # We don't want to skip the write unless we're really + # sure that the existing cache is identical, so don't + # trust _mtime_ and _eclasses_ alone. + for k in auxdbkeys: + if dest.get(k, '') != src.get(k, ''): + dest = None + break + + if dest is not None: + # The existing data is valid and identical, + # so there's no need to overwrite it. + continue + + try: + tree_data.dest_db[cpv] = src + except CacheError: + # ignore it; can't do anything about it. + pass + + curval += 1 + if onProgress is not None: + onProgress(maxval, curval) + + if onProgress is not None: + onProgress(maxval, curval) + + for tree_data in porttrees_data: + try: + dead_nodes = set(tree_data.dest_db) + except CacheError as e: + writemsg_level("Error listing cache entries for " + \ + "'%s': %s, continuing...\n" % (tree_data.path, e), + level=logging.ERROR, noiselevel=-1) + del e + else: + dead_nodes.difference_update(tree_data.valid_nodes) + for cpv in dead_nodes: + try: + del tree_data.dest_db[cpv] + except (KeyError, CacheError): + pass + + if not quiet: + # make sure the final progress is displayed + progressHandler.display() + print() + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + + portdb.flush_cache() + sys.stdout.flush() + os.umask(old_umask) diff --git a/pym/portage/module.py b/pym/portage/module.py new file mode 100644 index 0000000..d961574 --- /dev/null +++ b/pym/portage/module.py @@ -0,0 +1,186 @@ +# Copyright 2005-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + + +from __future__ import print_function + +from portage import os +from portage.exception import PortageException +from portage.cache.mappings import ProtectedDict + + +class InvalidModuleName(PortageException): + """An invalid or unknown module name.""" + + +class Module(object): + """Class to define and hold our plug-in module + + @type name: string + @param name: the module name + @type path: the path to the new module + """ + + def __init__(self, name, namepath): + """Some variables initialization""" + self.name = name + self._namepath = namepath + self.kids_names = [] + self.kids = {} + self.initialized = self._initialize() + + def _initialize(self): + """Initialize the plug-in module + + @rtype: boolean + """ + self.valid = False + try: + mod_name = ".".join([self._namepath, self.name]) + self._module = __import__(mod_name, [], [], ["not empty"]) + self.valid = True + except ImportError as e: + print("MODULE; failed import", mod_name, " error was:", e) + return False + self.module_spec = self._module.module_spec + for submodule in self.module_spec['provides']: + kid = self.module_spec['provides'][submodule] + kidname = kid['name'] + kid['module_name'] = '.'.join([mod_name, self.name]) + kid['is_imported'] = False + self.kids[kidname] = kid + self.kids_names.append(kidname) + return True + + def get_class(self, name): + if not name or name not in self.kids_names: + raise InvalidModuleName("Module name '%s' was invalid or not" + %name + "part of the module '%s'" %self.name) + kid = self.kids[name] + if kid['is_imported']: + module = kid['instance'] + else: + try: + module = __import__(kid['module_name'], [], [], ["not empty"]) + kid['instance'] = module + kid['is_imported'] = True + except ImportError: + raise + mod_class = getattr(module, kid['class']) + return mod_class + + +class Modules(object): + """Dynamic modules system for loading and retrieving any of the + installed emaint modules and/or provided class's + + @param path: Optional path to the "modules" directory or + defaults to the directory of this file + '/modules' + @param namepath: Optional python import path to the "modules" directory or + defaults to the directory name of this file + '.modules' + """ + + def __init__(self, path, namepath): + self._module_path = path + self._namepath = namepath + self._modules = self._get_all_modules() + self.modules = ProtectedDict(self._modules) + self.module_names = sorted(self._modules) + #self.modules = {} + #for mod in self.module_names: + #self.module[mod] = LazyLoad( + + def _get_all_modules(self): + """scans the emaint modules dir for loadable modules + + @rtype: dictionary of module_plugins + """ + module_dir = self._module_path + importables = [] + names = os.listdir(module_dir) + for entry in names: + # skip any __init__ or __pycache__ files or directories + if entry.startswith('__'): + continue + try: + # test for statinfo to ensure it should a real module + # it will bail if it errors + os.lstat(os.path.join(module_dir, entry, '__init__.py')) + importables.append(entry) + except EnvironmentError: + pass + kids = {} + for entry in importables: + new_module = Module(entry, self._namepath) + for module_name in new_module.kids: + kid = new_module.kids[module_name] + kid['parent'] = new_module + kids[kid['name']] = kid + return kids + + def get_module_names(self): + """Convienence function to return the list of installed modules + available + + @rtype: list + @return: the installed module names available + """ + return self.module_names + + def get_class(self, modname): + """Retrieves a module class desired + + @type modname: string + @param modname: the module class name + """ + if modname and modname in self.module_names: + mod = self._modules[modname]['parent'].get_class(modname) + else: + raise InvalidModuleName("Module name '%s' was invalid or not" + %modname + "found") + return mod + + def get_description(self, modname): + """Retrieves the module class decription + + @type modname: string + @param modname: the module class name + @type string + @return: the modules class decription + """ + if modname and modname in self.module_names: + mod = self._modules[modname]['description'] + else: + raise InvalidModuleName("Module name '%s' was invalid or not" + %modname + "found") + return mod + + def get_functions(self, modname): + """Retrieves the module class exported function names + + @type modname: string + @param modname: the module class name + @type list + @return: the modules class exported function names + """ + if modname and modname in self.module_names: + mod = self._modules[modname]['functions'] + else: + raise InvalidModuleName("Module name '%s' was invalid or not" + %modname + "found") + return mod + + def get_func_descriptions(self, modname): + """Retrieves the module class exported functions descriptions + + @type modname: string + @param modname: the module class name + @type dictionary + @return: the modules class exported functions descriptions + """ + if modname and modname in self.module_names: + desc = self._modules[modname]['func_desc'] + else: + raise InvalidModuleName("Module name '%s' was invalid or not" + %modname + "found") + return desc diff --git a/pym/portage/progress.py b/pym/portage/progress.py new file mode 100644 index 0000000..e43c2af --- /dev/null +++ b/pym/portage/progress.py @@ -0,0 +1,61 @@ +# Copyright 2005-2012 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import time +import signal + +import portage + + +class ProgressHandler(object): + def __init__(self): + self.reset() + + def reset(self): + self.curval = 0 + self.maxval = 0 + self.last_update = 0 + self.min_display_latency = 0.2 + + def onProgress(self, maxval, curval): + self.maxval = maxval + self.curval = curval + cur_time = time.time() + if cur_time - self.last_update >= self.min_display_latency: + self.last_update = cur_time + self.display() + + def display(self): + raise NotImplementedError(self) + + +class ProgressBar(ProgressHandler): + """Class to set up and return a Progress Bar""" + + def __init__(self, isatty, **kwargs): + self.isatty = isatty + self.kwargs = kwargs + ProgressHandler.__init__(self) + self.progressBar = None + + def start(self): + if self.isatty: + self.progressBar = portage.output.TermProgressBar(**self.kwargs) + signal.signal(signal.SIGWINCH, self.sigwinch_handler) + else: + self.onProgress = None + return self.onProgress + + def set_label(self, _label): + self.kwargs['label'] = _label + + def display(self): + self.progressBar.set(self.curval, self.maxval) + + def sigwinch_handler(self, signum, frame): + lines, self.progressBar.term_columns = \ + portage.output.get_term_size() + + def stop(self): + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + diff --git a/pym/portage/repository/config.py b/pym/portage/repository/config.py index 5e0d055..10bd477 100644 --- a/pym/portage/repository/config.py +++ b/pym/portage/repository/config.py @@ -31,6 +31,7 @@ from portage import _unicode_decode from portage import _unicode_encode from portage import _encodings from portage import manifest +import portage.sync if sys.hexversion >= 0x3000000: # pylint: disable=W0622 @@ -78,8 +79,8 @@ class RepoConfig(object): """Stores config of one repository""" __slots__ = ('aliases', 'allow_missing_manifest', 'allow_provide_virtual', - 'cache_formats', 'create_manifest', 'disable_manifest', 'eapi', - 'eclass_db', 'eclass_locations', 'eclass_overrides', + 'auto_sync', 'cache_formats', 'create_manifest', 'disable_manifest', + 'eapi', 'eclass_db', 'eclass_locations', 'eclass_overrides', 'find_invalid_path_char', 'force', 'format', 'local_config', 'location', 'main_repo', 'manifest_hashes', 'masters', 'missing_repo_name', 'name', 'portage1_profiles', 'portage1_profiles_compat', 'priority', @@ -158,6 +159,11 @@ class RepoConfig(object): sync_uri = sync_uri.strip() self.sync_uri = sync_uri or None + auto_sync = repo_opts.get('auto-sync') + if auto_sync is not None: + auto_sync = auto_sync.strip().lower() + self.auto_sync = auto_sync + # Not implemented. format = repo_opts.get('format') if format is not None: @@ -456,8 +462,10 @@ class RepoConfigLoader(object): if repos_conf_opts is not None: # Selectively copy only the attributes which # repos.conf is allowed to override. - for k in ('aliases', 'eclass_overrides', 'force', 'masters', - 'priority', 'sync_cvs_repo', 'sync_type', 'sync_uri'): + for k in ('aliases', 'auto_sync', 'eclass_overrides', + 'force', 'masters', 'priority', 'sync_cvs_repo', + 'sync_type', 'sync_uri', + ): v = getattr(repos_conf_opts, k, None) if v is not None: setattr(repo, k, v) @@ -550,25 +558,8 @@ class RepoConfigLoader(object): repo = RepoConfig(sname, optdict, local_config=local_config) - if repo.sync_type is not None and repo.sync_uri is None: - writemsg_level("!!! %s\n" % _("Repository '%s' has sync-type attribute, but is missing sync-uri attribute") % - sname, level=logging.ERROR, noiselevel=-1) - continue - - if repo.sync_uri is not None and repo.sync_type is None: - writemsg_level("!!! %s\n" % _("Repository '%s' has sync-uri attribute, but is missing sync-type attribute") % - sname, level=logging.ERROR, noiselevel=-1) - continue - - if repo.sync_type not in (None, "cvs", "git", "rsync"): - writemsg_level("!!! %s\n" % _("Repository '%s' has sync-type attribute set to unsupported value: '%s'") % - (sname, repo.sync_type), level=logging.ERROR, noiselevel=-1) - continue - - if repo.sync_type == "cvs" and repo.sync_cvs_repo is None: - writemsg_level("!!! %s\n" % _("Repository '%s' has sync-type=cvs, but is missing sync-cvs-repo attribute") % - sname, level=logging.ERROR, noiselevel=-1) - continue + # Perform repos.conf sync variable validation + portage.sync.validate_config(repo, logging) # For backward compatibility with locations set via PORTDIR and # PORTDIR_OVERLAY, delay validation of the location and repo.name diff --git a/pym/portage/sync/__init__.py b/pym/portage/sync/__init__.py new file mode 100644 index 0000000..58a1298 --- /dev/null +++ b/pym/portage/sync/__init__.py @@ -0,0 +1,54 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import os + +from portage.module import Modules +from portage.sync.controller import SyncManager +from portage.sync.config_checks import check_type + +sync_manager = None + +path = os.path.join(os.path.dirname(__file__), "modules") +# initial development debug info +#print("module path:", path) + +module_controller = Modules(path=path, namepath="portage.sync.modules") + +# initial development debug info +#print(module_controller.module_names) +module_names = module_controller.module_names[:] + + +def get_syncer(settings=None, logger=None): + '''Initializes and returns the SyncManager instance + to be used for sync operations + + @param settings: emerge.settings instance + @param logger: emerge logger instance + @returns SyncManager instance + ''' + global sync_manager + if sync_manager and not settings and not logger: + return sync_manager + if settings is None: + from _emerge.actions import load_emerge_config + emerge_config = load_emerge_config() + settings = emerge_config.target_config.settings + if logger is None: + from _emerge.emergelog import emergelog as logger + sync_manager = SyncManager(settings, logger) + return sync_manager + + +def validate_config(repo, logger): + '''Validate the repos.conf settings for the repo''' + global module_names, module_controller + if not check_type(repo, logger, module_names): + return False + + #print(repo) + if repo.sync_type: + validated = module_controller.modules[repo.sync_type]['validate_config'] + return validated(repo, logger).repo_checks() + return True diff --git a/pym/portage/sync/config_checks.py b/pym/portage/sync/config_checks.py new file mode 100644 index 0000000..db316aa --- /dev/null +++ b/pym/portage/sync/config_checks.py @@ -0,0 +1,72 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +''' +Base class for performing repos.conf sync variables checks. +This class contains common checks code and functions. + +For additional checks or other customizations, +subclass it adding and/or overriding classes as needed. +''' + +import logging + +from portage.localization import _ +from portage.util import writemsg_level + + +def check_type(repo, logger, module_names): + if repo.sync_uri is not None and repo.sync_type is None: + writemsg_level("!!! %s\n" % + _("Repository '%s' has sync-uri attribute, but is missing sync-type attribute") + % repo.name, level=logger.ERROR, noiselevel=-1) + return False + if repo.sync_type not in module_names + [None]: + writemsg_level("!!! %s\n" % + _("Repository '%s' has sync-type attribute set to unsupported value: '%s'") + % (repo.name, repo.sync_type), + level=logger.ERROR, noiselevel=-1) + writemsg_level("!!! %s\n" % + _("Installed sync-types are: '%s'") + % (str(module_names)), + level=logger.ERROR, noiselevel=-1) + return False + return True + + +class CheckSyncConfig(object): + '''Base repos.conf settings checks class''' + + def __init__(self, repo=None, logger=None): + '''Class init function + + @param logger: optional logging instance, + defaults to logging module + ''' + self.logger = logger or logging + self.repo = repo + self.checks = ['check_uri', 'check_auto_sync'] + + + def repo_checks(self): + '''Perform all checks available''' + for check in self.checks: + getattr(self, check)() + + + def check_uri(self): + '''Check the sync_uri setting''' + if self.repo.sync_uri is None: + writemsg_level("!!! %s\n" % _("Repository '%s' has sync-type attribute, but is missing sync-uri attribute") + % self.repo.name, level=self.logger.ERROR, noiselevel=-1) + + + def check_auto_sync(self): + '''Check the auto_sync setting''' + if self.repo.auto_sync is None: + writemsg_level("!!! %s\n" % _("Repository '%s' is missing auto_sync attribute") + % self.repo.name, level=self.logger.ERROR, noiselevel=-1) + elif self.repo.auto_sync.lower() not in ["yes", "true", "no", "false"]: + writemsg_level("!!! %s\n" % _("Repository '%s' auto_sync attribute must be one of: %s") + % (self.repo.name, '{yes, true, no, false}'), + level=self.logger.ERROR, noiselevel=-1) diff --git a/pym/portage/sync/controller.py b/pym/portage/sync/controller.py new file mode 100644 index 0000000..0fe723b --- /dev/null +++ b/pym/portage/sync/controller.py @@ -0,0 +1,220 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from __future__ import print_function + + +import sys +import logging +import pwd + +import portage +from portage import os +from portage.progress import ProgressBar +#from portage.emaint.defaults import DEFAULT_OPTIONS +#from portage.util._argparse import ArgumentParser +from portage.util import writemsg_level +from portage.output import create_color_func +good = create_color_func("GOOD") +bad = create_color_func("BAD") +warn = create_color_func("WARN") +from portage.package.ebuild.doebuild import _check_temp_dir +from portage.metadata import action_metadata + + +class TaskHandler(object): + """Handles the running of the tasks it is given + """ + + def __init__(self, show_progress_bar=True, verbose=True, callback=None): + self.show_progress_bar = show_progress_bar + self.verbose = verbose + self.callback = callback + self.isatty = os.environ.get('TERM') != 'dumb' and sys.stdout.isatty() + self.progress_bar = ProgressBar(self.isatty, title="Portage-Sync", max_desc_length=27) + + + def run_tasks(self, tasks, func, status=None, verbose=True, options=None): + """Runs the module tasks""" + if tasks is None or func is None: + return + for task in tasks: + inst = task() + show_progress = self.show_progress_bar and self.isatty + # check if the function is capable of progressbar + # and possibly override it off + if show_progress and hasattr(inst, 'can_progressbar'): + show_progress = inst.can_progressbar(func) + if show_progress: + self.progress_bar.reset() + self.progress_bar.set_label(func + " " + inst.name()) + onProgress = self.progress_bar.start() + else: + onProgress = None + kwargs = { + 'onProgress': onProgress, + # pass in a copy of the options so a module can not pollute or change + # them for other tasks if there is more to do. + 'options': options.copy() + } + result = getattr(inst, func)(**kwargs) + if show_progress: + # make sure the final progress is displayed + self.progress_bar.display() + print() + self.progress_bar.stop() + if self.callback: + self.callback(result) + + +def print_results(results): + if results: + print() + print("\n".join(results)) + print("\n") + + +class SyncManager(object): + '''Main sync control module''' + + def __init__(self, settings, logger): + self.logger = logger + # Similar to emerge, sync needs a default umask so that created + # files have sane permissions. + os.umask(0o22) + + self.module_controller = portage.sync.module_controller + self.module_names = self.module_controller.module_names[:] + + + def get_module_descriptions(self, mod): + desc = self.module_controller.get_func_descriptions(mod) + if desc: + return desc + return [] + + + def sync(self, emerge_config=None, repo=None, callback=None): + self.emerge_config = emerge_config + self.callback = callback or self._sync_callback + self.repo = repo + self.exitcode = 1 + if repo.sync_type in self.module_names[1:]: + tasks = [self.module_controller.get_class(repo.sync_type)] + else: + portage.util.writemsg( + "\nERROR: Sync module '%s' is not an installed/known type'\n\n" + % (repo.sync_type), noiselevel=-1) + return self.exitcode + + rval = self.pre_sync(repo) + if rval != os.EX_OK: + return rval + + # need to pass the kwargs dict to the modules + # so they are available if needed. + task_opts = { + 'emerge_config': emerge_config, + 'logger': self.logger, + 'portdb': self.trees[self.settings['EROOT']]['porttree'].dbapi, + 'repo': repo, + 'settings': self.settings, + 'spawn_kwargs': self.spawn_kwargs, + 'usersync_uid': self.usersync_uid, + 'xterm_titles': self.xterm_titles, + } + func = 'sync' + status = None + taskmaster = TaskHandler(callback=self.do_callback) + taskmaster.run_tasks(tasks, func, status, options=task_opts) + + self.perform_post_sync_hook(repo.sync_uri) + + return self.exitcode + + + def do_callback(self, result): + #print("result:", result, "callback()", self.callback) + exitcode, updatecache_flg = result + if self.callback: + self.callback(exitcode, updatecache_flg) + return + + + def perform_post_sync_hook(self, dosyncuri): + postsync = os.path.join(self.settings["PORTAGE_CONFIGROOT"], + portage.USER_CONFIG_PATH, "bin", "post_sync") + if os.access(postsync, os.X_OK): + retval = portage.process.spawn([postsync, dosyncuri], + env=self.settings.environ()) + if retval != os.EX_OK: + writemsg_level(" %s spawn failed of %s\n" % (bad("*"), + postsync,), level=logging.ERROR, noiselevel=-1) + return retval + return os.EX_OK + + + def pre_sync(self, repo): + self.settings, self.trees, self.mtimedb = self.emerge_config + self.xterm_titles = "notitles" not in self.settings.features + msg = ">>> Synchronization of repository '%s' located in '%s'..." \ + % (repo.name, repo.location) + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + try: + st = os.stat(repo.location) + except OSError: + st = None + if st is None: + print(">>> '%s' not found, creating it." % repo.location) + portage.util.ensure_dirs(repo.location, mode=0o755) + st = os.stat(repo.location) + + self.usersync_uid = None + spawn_kwargs = {} + spawn_kwargs["env"] = self.settings.environ() + if 'usersync' in self.settings.features and \ + portage.data.secpass >= 2 and \ + (st.st_uid != os.getuid() and st.st_mode & 0o700 or \ + st.st_gid != os.getgid() and st.st_mode & 0o070): + try: + homedir = pwd.getpwuid(st.st_uid).pw_dir + except KeyError: + pass + else: + # Drop privileges when syncing, in order to match + # existing uid/gid settings. + self.usersync_uid = st.st_uid + spawn_kwargs["uid"] = st.st_uid + spawn_kwargs["gid"] = st.st_gid + spawn_kwargs["groups"] = [st.st_gid] + spawn_kwargs["env"]["HOME"] = homedir + umask = 0o002 + if not st.st_mode & 0o020: + umask = umask | 0o020 + spawn_kwargs["umask"] = umask + self.spawn_kwargs = spawn_kwargs + + if self.usersync_uid is not None: + # PORTAGE_TMPDIR is used below, so validate it and + # bail out if necessary. + rval = _check_temp_dir(self.settings) + if rval != os.EX_OK: + return rval + + os.umask(0o022) + return os.EX_OK + + + def _sync_callback(self, exitcode, updatecache_flg): + if updatecache_flg and "metadata-transfer" not in self.settings.features: + updatecache_flg = False + + if updatecache_flg and \ + os.path.exists(os.path.join(self.repo.location, 'metadata', 'cache')): + + # Only update cache for repo.location since that's + # the only one that's been synced here. + action_metadata(self.settings, self.portdb, self.emerge_config.opts, + porttrees=[self.repo.location]) + diff --git a/pym/portage/sync/getaddrinfo_validate.py b/pym/portage/sync/getaddrinfo_validate.py new file mode 100644 index 0000000..5e6009c --- /dev/null +++ b/pym/portage/sync/getaddrinfo_validate.py @@ -0,0 +1,29 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import sys + +if sys.hexversion >= 0x3000000: + basestring = str + +def getaddrinfo_validate(addrinfos): + """ + Validate structures returned from getaddrinfo(), + since they may be corrupt, especially when python + has IPv6 support disabled (bug #340899). + """ + valid_addrinfos = [] + for addrinfo in addrinfos: + try: + if len(addrinfo) != 5: + continue + if len(addrinfo[4]) < 2: + continue + if not isinstance(addrinfo[4][0], basestring): + continue + except TypeError: + continue + + valid_addrinfos.append(addrinfo) + + return valid_addrinfos diff --git a/pym/portage/sync/modules/__init__.py b/pym/portage/sync/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pym/portage/sync/modules/cvs/__init__.py b/pym/portage/sync/modules/cvs/__init__.py new file mode 100644 index 0000000..fd3b12a --- /dev/null +++ b/pym/portage/sync/modules/cvs/__init__.py @@ -0,0 +1,46 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +"""CVS plug-in module for portage. +Performs a cvs up on repositories +""" + + +from portage.localization import _ +from portage.sync.config_checks import CheckSyncConfig +from portage.util import writemsg_level + + +class CheckCVSConfig(CheckSyncConfig): + + def __init__(self, logger): + CheckSyncConfig.__init__(self, logger) + self.checks.append('check_cvs_repo') + + + def check_cvs_repo(self): + if self.repo.sync_cvs_repo is None: + writemsg_level("!!! %s\n" % + _("Repository '%s' has sync-type=cvs, but is missing sync-cvs-repo attribute") + % self.repo.name, level=self.logger.ERROR, noiselevel=-1) + + +module_spec = { + 'name': 'cvs', + 'description': __doc__, + 'provides':{ + 'cvs-module': { + 'name': "cvs", + 'class': "CVSSync", + 'description': __doc__, + 'functions': ['sync', 'new', 'exists'], + 'func_desc': { + 'sync': 'Performs a cvs up on the repository', + 'new': 'Creates the new repository at the specified location', + 'exists': 'Returns a boolean of whether the specified dir ' + + 'exists and is a valid CVS repository', + }, + 'validate_config': CheckCVSConfig, + } + } +} diff --git a/pym/portage/sync/modules/cvs/cvs.py b/pym/portage/sync/modules/cvs/cvs.py new file mode 100644 index 0000000..52d5d9a --- /dev/null +++ b/pym/portage/sync/modules/cvs/cvs.py @@ -0,0 +1,82 @@ +# Copyright 1999-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import logging +import errno + +import portage +from portage import os +from portage.util import writemsg_level +from portage.sync.syncbase import SyncBase + + +class CVSSync(SyncBase): + '''CVS sync module''' + + short_desc = "Perform sync operations on CVS repositories" + + @staticmethod + def name(): + return "CVSSync" + + + def __init__(self): + SyncBase.__init__(self, "cvs", portage.const.CVS_PACKAGE_ATOM) + + + def new(self, **kwargs): + if kwargs: + self._kwargs(kwargs) + #initial checkout + msg = ">>> Starting initial cvs checkout with %s..." % self.repo.sync_uri + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + try: + os.rmdir(self.repo.location) + except OSError as e: + if e.errno != errno.ENOENT: + msg = "!!! existing '%s' directory; exiting." % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (1, False) + del e + cvs_root = self.repo.sync_uri + if portage.process.spawn_bash( + "cd %s; exec cvs -z0 -d %s co -P -d %s %s" % + (portage._shell_quote(os.path.dirname(self.repo.location)), portage._shell_quote(cvs_root), + portage._shell_quote(os.path.basename(self.repo.location)), + portage._shell_quote(self.repo.sync_cvs_repo)), + **portage._native_kwargs(self.spawn_kwargs)) != os.EX_OK: + msg = "!!! cvs checkout error; exiting." + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (1, False) + return (0, False) + + + def _sync(self): + """ + Internal function to sync an existing CVS repository + + @return: tuple of return code (0=success), whether the cache + needs to be updated + @rtype: (int, bool) + """ + + cvs_root = self.repo.sync_uri + + if cvs_root.startswith("cvs://"): + cvs_root = cvs_root[6:] + #cvs update + msg = ">>> Starting cvs update with %s..." % self.repo.sync_uri + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + exitcode = portage.process.spawn_bash( + "cd %s; exec cvs -z0 -q update -dP" % \ + (portage._shell_quote(self.repo.location),), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! cvs update error; exiting." + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (exitcode, False) diff --git a/pym/portage/sync/modules/git/__init__.py b/pym/portage/sync/modules/git/__init__.py new file mode 100644 index 0000000..304142e --- /dev/null +++ b/pym/portage/sync/modules/git/__init__.py @@ -0,0 +1,30 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +"""Git plug-in module for portage. +Performs a git pull on repositories +""" + + +from portage.sync.config_checks import CheckSyncConfig + + +module_spec = { + 'name': 'git', + 'description': __doc__, + 'provides':{ + 'git-module': { + 'name': "git", + 'class': "GitSync", + 'description': __doc__, + 'functions': ['sync', 'new', 'exists'], + 'func_desc': { + 'sync': 'Performs a git pull on the repository', + 'new': 'Creates the new repository at the specified location', + 'exists': 'Returns a boolean of whether the specified dir ' + + 'exists and is a valid Git repository', + }, + 'validate_config': CheckSyncConfig, + } + } +} diff --git a/pym/portage/sync/modules/git/git.py b/pym/portage/sync/modules/git/git.py new file mode 100644 index 0000000..5e0e5df --- /dev/null +++ b/pym/portage/sync/modules/git/git.py @@ -0,0 +1,124 @@ +# Copyright 2005-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import logging + +import portage +from portage import os +from portage.util import writemsg_level +from portage.output import create_color_func +good = create_color_func("GOOD") +bad = create_color_func("BAD") +warn = create_color_func("WARN") +from .timestamps import git_sync_timestamps +from portage.sync.syncbase import SyncBase + + +class GitSync(SyncBase): + '''Git sync class''' + + short_desc = "Perform sync operations on git based repositories" + + @staticmethod + def name(): + return "GitSync" + + + def __init__(self): + SyncBase.__init__(self, "git", portage.const.GIT_PACKAGE_ATOM) + + + def exists(self, **kwargs): + '''Tests whether the repo actually exists''' + if kwargs: + self._kwargs(kwargs) + elif not self.repo: + return False + + if not os.path.exists(self.repo.location): + return False + exitcode = portage.process.spawn_bash("cd %s ; git rev-parse" %\ + (portage._shell_quote(self.repo.location),), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode == 128: + return False + return True + + + def new(self, **kwargs): + '''Do the initial clone of the repository''' + if kwargs: + self._kwargs(kwargs) + emerge_config = self.options.get('emerge_config', None) + portdb = self.options.get('portdb', None) + try: + if not os.path.exists(self.repo.location): + os.makedirs(self.repo.location) + self.logger(self.xterm_titles, + 'Created new directory %s' % self.repo.location) + except IOError: + return (1, False) + msg = ">>> Cloning git repository from upstream into %s..." % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + exitcode = portage.process.spawn_bash("cd %s ; %s clone %s ." % \ + (portage._shell_quote(self.repo.location), + self.bin_command, + portage._shell_quote(self.repo.sync_uri)), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! git clone error in %s" % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1) + return (exitcode, False) + msg = ">>> Git clone successful" + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + return self.post_sync(portdb, self.repo.location, emerge_config) + + + def _sync(self): + ''' Update existing git repository, and ignore the syncuri. We are + going to trust the user and assume that the user is in the branch + that he/she wants updated. We'll let the user manage branches with + git directly. + ''' + # No kwargs call here; this is internal, so it should have been + # called by something which set the internal variables + emerge_config = self.options.get('emerge_config', None) + portdb = self.options.get('portdb', None) + + msg = ">>> Starting git pull in %s..." % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + exitcode = portage.process.spawn_bash("cd %s ; git pull" % \ + (portage._shell_quote(self.repo.location),), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! git pull error in %s" % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1) + return (exitcode, False) + msg = ">>> Git pull successful: %s" % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + return self.post_sync(portdb, self.repo.location, emerge_config) + + + def post_sync(self, portdb, location, emerge_config): + '''repo.sync_type == "git": + # NOTE: Do this after reloading the config, in case + # it did not exist prior to sync, so that the config + # and portdb properly account for its existence. + ''' + # avoid circular import for now + from _emerge.actions import load_emerge_config, adjust_configs + # Reload the whole config from scratch. + settings, trees, mtimedb = load_emerge_config(emerge_config=emerge_config) + adjust_configs(emerge_config.opts, emerge_config.trees) + portdb = trees[settings['EROOT']]['porttree'].dbapi + updatecache_flg = False + exitcode = git_sync_timestamps(portdb, location) + if exitcode == os.EX_OK: + updatecache_flg = True + return (exitcode, updatecache_flg) diff --git a/pym/portage/sync/modules/git/timestamps.py b/pym/portage/sync/modules/git/timestamps.py new file mode 100644 index 0000000..5e44845 --- /dev/null +++ b/pym/portage/sync/modules/git/timestamps.py @@ -0,0 +1,154 @@ +# Copyright 1999-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import logging +import stat +import subprocess + + +import portage +from portage import os +from portage import _unicode_decode +from portage.util import writemsg_level +from portage.cache.cache_errors import CacheError + + +def git_sync_timestamps(portdb, portdir): + """ + Since git doesn't preserve timestamps, synchronize timestamps between + entries and ebuilds/eclasses. Assume the cache has the correct timestamp + for a given file as long as the file in the working tree is not modified + (relative to HEAD). + """ + + cache_db = portdb._pregen_auxdb.get(portdir) + + try: + if cache_db is None: + # portdbapi does not populate _pregen_auxdb + # when FEATURES=metadata-transfer is enabled + cache_db = portdb._create_pregen_cache(portdir) + except CacheError as e: + writemsg_level("!!! Unable to instantiate cache: %s\n" % (e,), + level=logging.ERROR, noiselevel=-1) + return 1 + + if cache_db is None: + return os.EX_OK + + if cache_db.validation_chf != 'mtime': + # newer formats like md5-dict do not require mtime sync + return os.EX_OK + + writemsg_level(">>> Synchronizing timestamps...\n") + + ec_dir = os.path.join(portdir, "eclass") + try: + ec_names = set(f[:-7] for f in os.listdir(ec_dir) \ + if f.endswith(".eclass")) + except OSError as e: + writemsg_level("!!! Unable to list eclasses: %s\n" % (e,), + level=logging.ERROR, noiselevel=-1) + return 1 + + args = [portage.const.BASH_BINARY, "-c", + "cd %s && git diff-index --name-only --diff-filter=M HEAD" % \ + portage._shell_quote(portdir)] + proc = subprocess.Popen(args, stdout=subprocess.PIPE) + modified_files = set(_unicode_decode(l).rstrip("\n") for l in proc.stdout) + rval = proc.wait() + proc.stdout.close() + if rval != os.EX_OK: + return rval + + modified_eclasses = set(ec for ec in ec_names \ + if os.path.join("eclass", ec + ".eclass") in modified_files) + + updated_ec_mtimes = {} + + for cpv in cache_db: + cpv_split = portage.catpkgsplit(cpv) + if cpv_split is None: + writemsg_level("!!! Invalid cache entry: %s\n" % (cpv,), + level=logging.ERROR, noiselevel=-1) + continue + + cat, pn, ver, rev = cpv_split + cat, pf = portage.catsplit(cpv) + relative_eb_path = os.path.join(cat, pn, pf + ".ebuild") + if relative_eb_path in modified_files: + continue + + try: + cache_entry = cache_db[cpv] + eb_mtime = cache_entry.get("_mtime_") + ec_mtimes = cache_entry.get("_eclasses_") + except KeyError: + writemsg_level("!!! Missing cache entry: %s\n" % (cpv,), + level=logging.ERROR, noiselevel=-1) + continue + except CacheError as e: + writemsg_level("!!! Unable to access cache entry: %s %s\n" % \ + (cpv, e), level=logging.ERROR, noiselevel=-1) + continue + + if eb_mtime is None: + writemsg_level("!!! Missing ebuild mtime: %s\n" % (cpv,), + level=logging.ERROR, noiselevel=-1) + continue + + try: + eb_mtime = long(eb_mtime) + except ValueError: + writemsg_level("!!! Invalid ebuild mtime: %s %s\n" % \ + (cpv, eb_mtime), level=logging.ERROR, noiselevel=-1) + continue + + if ec_mtimes is None: + writemsg_level("!!! Missing eclass mtimes: %s\n" % (cpv,), + level=logging.ERROR, noiselevel=-1) + continue + + if modified_eclasses.intersection(ec_mtimes): + continue + + missing_eclasses = set(ec_mtimes).difference(ec_names) + if missing_eclasses: + writemsg_level("!!! Non-existent eclass(es): %s %s\n" % \ + (cpv, sorted(missing_eclasses)), level=logging.ERROR, + noiselevel=-1) + continue + + eb_path = os.path.join(portdir, relative_eb_path) + try: + current_eb_mtime = os.stat(eb_path) + except OSError: + writemsg_level("!!! Missing ebuild: %s\n" % \ + (cpv,), level=logging.ERROR, noiselevel=-1) + continue + + inconsistent = False + for ec, (ec_path, ec_mtime) in ec_mtimes.items(): + updated_mtime = updated_ec_mtimes.get(ec) + if updated_mtime is not None and updated_mtime != ec_mtime: + writemsg_level("!!! Inconsistent eclass mtime: %s %s\n" % \ + (cpv, ec), level=logging.ERROR, noiselevel=-1) + inconsistent = True + break + + if inconsistent: + continue + + if current_eb_mtime != eb_mtime: + os.utime(eb_path, (eb_mtime, eb_mtime)) + + for ec, (ec_path, ec_mtime) in ec_mtimes.items(): + if ec in updated_ec_mtimes: + continue + ec_path = os.path.join(ec_dir, ec + ".eclass") + current_mtime = os.stat(ec_path)[stat.ST_MTIME] + if current_mtime != ec_mtime: + os.utime(ec_path, (ec_mtime, ec_mtime)) + updated_ec_mtimes[ec] = ec_mtime + + return os.EX_OK diff --git a/pym/portage/sync/modules/rsync/__init__.py b/pym/portage/sync/modules/rsync/__init__.py new file mode 100644 index 0000000..9e19d85 --- /dev/null +++ b/pym/portage/sync/modules/rsync/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +"""Rsync plug-in module for portage. + Performs rsync transfers on repositories +""" + + +from portage.sync.config_checks import CheckSyncConfig + + +module_spec = { + 'name': 'rsync', + 'description': __doc__, + 'provides':{ + 'rsync-module': { + 'name': "rsync", + 'class': "RsyncSync", + 'description': __doc__, + 'functions': ['sync', 'new', 'exists'], + 'func_desc': { + 'sync': 'Performs rsync transfers on the repository', + 'new': 'Creates the new repository at the specified location', + 'exists': 'Returns a boolean if the specified directory exists', + }, + 'validate_config': CheckSyncConfig, + } + } + } diff --git a/pym/portage/sync/modules/rsync/rsync.py b/pym/portage/sync/modules/rsync/rsync.py new file mode 100644 index 0000000..76d83f2 --- /dev/null +++ b/pym/portage/sync/modules/rsync/rsync.py @@ -0,0 +1,519 @@ +# Copyright 1999-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import sys +import logging +import time +import signal +import socket +import re +import random +import tempfile + +import portage +from portage import os +from portage.util import writemsg_level +from portage.output import create_color_func, yellow, blue, bold +good = create_color_func("GOOD") +bad = create_color_func("BAD") +warn = create_color_func("WARN") +from portage.const import VCS_DIRS, TIMESTAMP_FORMAT, RSYNC_PACKAGE_ATOM +from portage.util import writemsg, writemsg_stdout +from portage.sync.getaddrinfo_validate import getaddrinfo_validate +from _emerge.UserQuery import UserQuery +from portage.sync.syncbase import SyncBase + + +SERVER_OUT_OF_DATE = -1 +EXCEEDED_MAX_RETRIES = -2 + + +class RsyncSync(SyncBase): + '''Rsync sync module''' + + short_desc = "Perform sync operations on rsync based repositories" + + @staticmethod + def name(): + return "RsyncSync" + + + def __init__(self): + SyncBase.__init__(self, "rsync", RSYNC_PACKAGE_ATOM) + + + def _sync(self): + '''Internal sync function which performs only the sync''' + opts = self.options.get('emerge_config').opts + self.usersync_uid = self.options.get('usersync_uid', None) + enter_invalid = '--ask-enter-invalid' in opts + out = portage.output.EOutput() + syncuri = self.repo.sync_uri + vcs_dirs = frozenset(VCS_DIRS) + vcs_dirs = vcs_dirs.intersection(os.listdir(self.repo.location)) + + for vcs_dir in vcs_dirs: + writemsg_level(("!!! %s appears to be under revision " + \ + "control (contains %s).\n!!! Aborting rsync sync.\n") % \ + (self.repo.location, vcs_dir), level=logging.ERROR, noiselevel=-1) + return (1, False) + self.timeout=180 + + rsync_opts = [] + if self.settings["PORTAGE_RSYNC_OPTS"] == "": + rsync_opts = self._set_rsync_defaults() + else: + rsync_opts = self._validate_rsync_opts(rsync_opts, syncuri) + self.rsync_opts = self._rsync_opts_extend(opts, rsync_opts) + + # Real local timestamp file. + self.servertimestampfile = os.path.join( + self.repo.location, "metadata", "timestamp.chk") + + content = portage.util.grabfile(self.servertimestampfile) + timestamp = 0 + if content: + try: + timestamp = time.mktime(time.strptime(content[0], + TIMESTAMP_FORMAT)) + except (OverflowError, ValueError): + pass + del content + + try: + self.rsync_initial_timeout = \ + int(self.settings.get("PORTAGE_RSYNC_INITIAL_TIMEOUT", "15")) + except ValueError: + self.rsync_initial_timeout = 15 + + try: + maxretries=int(self.settings["PORTAGE_RSYNC_RETRIES"]) + except SystemExit as e: + raise # Needed else can't exit + except: + maxretries = -1 #default number of retries + + retries=0 + try: + self.proto, user_name, hostname, port = re.split( + r"(rsync|ssh)://([^:/]+@)?(\[[:\da-fA-F]*\]|[^:/]*)(:[0-9]+)?", + syncuri, maxsplit=4)[1:5] + except ValueError: + writemsg_level("!!! sync-uri is invalid: %s\n" % syncuri, + noiselevel=-1, level=logging.ERROR) + return (1, False) + + self.ssh_opts = self.settings.get("PORTAGE_SSH_OPTS") + + if port is None: + port="" + if user_name is None: + user_name="" + if re.match(r"^\[[:\da-fA-F]*\]$", hostname) is None: + getaddrinfo_host = hostname + else: + # getaddrinfo needs the brackets stripped + getaddrinfo_host = hostname[1:-1] + updatecache_flg=True + all_rsync_opts = set(self.rsync_opts) + self.extra_rsync_opts = portage.util.shlex_split( + self.settings.get("PORTAGE_RSYNC_EXTRA_OPTS","")) + all_rsync_opts.update(self.extra_rsync_opts) + + family = socket.AF_UNSPEC + if "-4" in all_rsync_opts or "--ipv4" in all_rsync_opts: + family = socket.AF_INET + elif socket.has_ipv6 and \ + ("-6" in all_rsync_opts or "--ipv6" in all_rsync_opts): + family = socket.AF_INET6 + + addrinfos = None + uris = [] + + try: + addrinfos = getaddrinfo_validate( + socket.getaddrinfo(getaddrinfo_host, None, + family, socket.SOCK_STREAM)) + except socket.error as e: + writemsg_level( + "!!! getaddrinfo failed for '%s': %s\n" % (hostname, e), + noiselevel=-1, level=logging.ERROR) + + if addrinfos: + + AF_INET = socket.AF_INET + AF_INET6 = None + if socket.has_ipv6: + AF_INET6 = socket.AF_INET6 + + ips_v4 = [] + ips_v6 = [] + + for addrinfo in addrinfos: + if addrinfo[0] == AF_INET: + ips_v4.append("%s" % addrinfo[4][0]) + elif AF_INET6 is not None and addrinfo[0] == AF_INET6: + # IPv6 addresses need to be enclosed in square brackets + ips_v6.append("[%s]" % addrinfo[4][0]) + + random.shuffle(ips_v4) + random.shuffle(ips_v6) + + # Give priority to the address family that + # getaddrinfo() returned first. + if AF_INET6 is not None and addrinfos and \ + addrinfos[0][0] == AF_INET6: + ips = ips_v6 + ips_v4 + else: + ips = ips_v4 + ips_v6 + + for ip in ips: + uris.append(syncuri.replace( + "//" + user_name + hostname + port + "/", + "//" + user_name + ip + port + "/", 1)) + + if not uris: + # With some configurations we need to use the plain hostname + # rather than try to resolve the ip addresses (bug #340817). + uris.append(syncuri) + + # reverse, for use with pop() + uris.reverse() + + effective_maxretries = maxretries + if effective_maxretries < 0: + effective_maxretries = len(uris) - 1 + + while (1): + if uris: + dosyncuri = uris.pop() + else: + writemsg("!!! Exhausted addresses for %s\n" % \ + hostname, noiselevel=-1) + return (1, False) + + if (retries==0): + if "--ask" in opts: + uq = UserQuery(opts) + if uq.query("Do you want to sync your Portage tree " + \ + "with the mirror at\n" + blue(dosyncuri) + bold("?"), + enter_invalid) == "No": + print() + print("Quitting.") + print() + sys.exit(128 + signal.SIGINT) + self.logger(self.xterm_titles, + ">>> Starting rsync with " + dosyncuri) + if "--quiet" not in opts: + print(">>> Starting rsync with "+dosyncuri+"...") + else: + self.logger(self.xterm_titles, + ">>> Starting retry %d of %d with %s" % \ + (retries, effective_maxretries, dosyncuri)) + writemsg_stdout( + "\n\n>>> Starting retry %d of %d with %s\n" % \ + (retries, effective_maxretries, dosyncuri), noiselevel=-1) + + if dosyncuri.startswith('ssh://'): + dosyncuri = dosyncuri[6:].replace('/', ':/', 1) + + is_synced, exitcode = self._do_rsync(dosyncuri, timestamp, opts) + if is_synced: + break + + retries=retries+1 + + if maxretries < 0 or retries <= maxretries: + print(">>> Retrying...") + else: + # over retries + # exit loop + updatecache_flg=False + exitcode = EXCEEDED_MAX_RETRIES + break + self._process_exitcode(exitcode, dosyncuri, out, maxretries) + return (exitcode, updatecache_flg) + + + def _process_exitcode(self, exitcode, syncuri, out, maxretries): + if (exitcode==0): + self.logger(self.xterm_titles, "=== Sync completed with %s" % syncuri) + elif exitcode == SERVER_OUT_OF_DATE: + exitcode = 1 + elif exitcode == EXCEEDED_MAX_RETRIES: + sys.stderr.write( + ">>> Exceeded PORTAGE_RSYNC_RETRIES: %s\n" % maxretries) + exitcode = 1 + elif (exitcode>0): + msg = [] + if exitcode==1: + msg.append("Rsync has reported that there is a syntax error. Please ensure") + msg.append("that sync-uri attribute for repository '%s' is proper." % self.repo.name) + msg.append("sync-uri: '%s'" % self.repo.sync_uri) + elif exitcode==11: + msg.append("Rsync has reported that there is a File IO error. Normally") + msg.append("this means your disk is full, but can be caused by corruption") + msg.append("on the filesystem that contains repository '%s'. Please investigate" % self.repo.name) + msg.append("and try again after the problem has been fixed.") + msg.append("Location of repository: '%s'" % self.repo.location) + elif exitcode==20: + msg.append("Rsync was killed before it finished.") + else: + msg.append("Rsync has not successfully finished. It is recommended that you keep") + msg.append("trying or that you use the 'emerge-webrsync' option if you are unable") + msg.append("to use rsync due to firewall or other restrictions. This should be a") + msg.append("temporary problem unless complications exist with your network") + msg.append("(and possibly your system's filesystem) configuration.") + for line in msg: + out.eerror(line) + + + def new(self, **kwargs): + if kwargs: + self._kwargs(kwargs) + try: + if not os.path.exists(self.repo.location): + os.makedirs(self.repo.location) + self.logger(self.self.xterm_titles, + 'Created New Directory %s ' % self.repo.location ) + except IOError: + return (1, False) + return self._sync() + + + def _set_rsync_defaults(self): + portage.writemsg("PORTAGE_RSYNC_OPTS empty or unset, using hardcoded defaults\n") + rsync_opts = [ + "--recursive", # Recurse directories + "--links", # Consider symlinks + "--safe-links", # Ignore links outside of tree + "--perms", # Preserve permissions + "--times", # Preserive mod times + "--omit-dir-times", + "--compress", # Compress the data transmitted + "--force", # Force deletion on non-empty dirs + "--whole-file", # Don't do block transfers, only entire files + "--delete", # Delete files that aren't in the master tree + "--stats", # Show final statistics about what was transfered + "--human-readable", + "--timeout="+str(self.timeout), # IO timeout if not done in X seconds + "--exclude=/distfiles", # Exclude distfiles from consideration + "--exclude=/local", # Exclude local from consideration + "--exclude=/packages", # Exclude packages from consideration + ] + return rsync_opts + + + def _validate_rsync_opts(self, rsync_opts, syncuri): + # The below validation is not needed when using the above hardcoded + # defaults. + + portage.writemsg("Using PORTAGE_RSYNC_OPTS instead of hardcoded defaults\n", 1) + rsync_opts.extend(portage.util.shlex_split( + self.settings.get("PORTAGE_RSYNC_OPTS", ""))) + for opt in ("--recursive", "--times"): + if opt not in rsync_opts: + portage.writemsg(yellow("WARNING:") + " adding required option " + \ + "%s not included in PORTAGE_RSYNC_OPTS\n" % opt) + rsync_opts.append(opt) + + for exclude in ("distfiles", "local", "packages"): + opt = "--exclude=/%s" % exclude + if opt not in rsync_opts: + portage.writemsg(yellow("WARNING:") + \ + " adding required option %s not included in " % opt + \ + "PORTAGE_RSYNC_OPTS (can be overridden with --exclude='!')\n") + rsync_opts.append(opt) + + if syncuri.rstrip("/").endswith(".gentoo.org/gentoo-portage"): + def rsync_opt_startswith(opt_prefix): + for x in rsync_opts: + if x.startswith(opt_prefix): + return (1, False) + return (0, False) + + if not rsync_opt_startswith("--timeout="): + rsync_opts.append("--timeout=%d" % self.timeout) + + for opt in ("--compress", "--whole-file"): + if opt not in rsync_opts: + portage.writemsg(yellow("WARNING:") + " adding required option " + \ + "%s not included in PORTAGE_RSYNC_OPTS\n" % opt) + rsync_opts.append(opt) + return rsync_opts + + + @staticmethod + def _rsync_opts_extend(opts, rsync_opts): + if "--quiet" in opts: + rsync_opts.append("--quiet") # Shut up a lot + else: + rsync_opts.append("--verbose") # Print filelist + + if "--verbose" in opts: + rsync_opts.append("--progress") # Progress meter for each file + + if "--debug" in opts: + rsync_opts.append("--checksum") # Force checksum on all files + return rsync_opts + + + def _do_rsync(self, syncuri, timestamp, opts): + is_synced = False + if timestamp != 0 and "--quiet" not in opts: + print(">>> Checking server timestamp ...") + + rsynccommand = [self.bin_command] + self.rsync_opts + self.extra_rsync_opts + + if self.proto == 'ssh' and self.ssh_opts: + rsynccommand.append("--rsh=ssh " + self.ssh_opts) + + if "--debug" in opts: + print(rsynccommand) + + exitcode = os.EX_OK + servertimestamp = 0 + # Even if there's no timestamp available locally, fetch the + # timestamp anyway as an initial probe to verify that the server is + # responsive. This protects us from hanging indefinitely on a + # connection attempt to an unresponsive server which rsync's + # --timeout option does not prevent. + + #if True: + # Temporary file for remote server timestamp comparison. + # NOTE: If FEATURES=usersync is enabled then the tempfile + # needs to be in a directory that's readable by the usersync + # user. We assume that PORTAGE_TMPDIR will satisfy this + # requirement, since that's not necessarily true for the + # default directory used by the tempfile module. + if self.usersync_uid is not None: + tmpdir = self.settings['PORTAGE_TMPDIR'] + else: + # use default dir from tempfile module + tmpdir = None + fd, tmpservertimestampfile = \ + tempfile.mkstemp(dir=tmpdir) + os.close(fd) + if self.usersync_uid is not None: + portage.util.apply_permissions(tmpservertimestampfile, + uid=self.usersync_uid) + command = rsynccommand[:] + command.append(syncuri.rstrip("/") + \ + "/metadata/timestamp.chk") + command.append(tmpservertimestampfile) + content = None + pids = [] + try: + # Timeout here in case the server is unresponsive. The + # --timeout rsync option doesn't apply to the initial + # connection attempt. + try: + if self.rsync_initial_timeout: + portage.exception.AlarmSignal.register( + self.rsync_initial_timeout) + + pids.extend(portage.process.spawn( + command, returnpid=True, + **portage._native_kwargs(self.spawn_kwargs))) + exitcode = os.waitpid(pids[0], 0)[1] + if self.usersync_uid is not None: + portage.util.apply_permissions(tmpservertimestampfile, + uid=os.getuid()) + content = portage.grabfile(tmpservertimestampfile) + finally: + if self.rsync_initial_timeout: + portage.exception.AlarmSignal.unregister() + try: + os.unlink(tmpservertimestampfile) + except OSError: + pass + except portage.exception.AlarmSignal: + # timed out + print('timed out') + # With waitpid and WNOHANG, only check the + # first element of the tuple since the second + # element may vary (bug #337465). + if pids and os.waitpid(pids[0], os.WNOHANG)[0] == 0: + os.kill(pids[0], signal.SIGTERM) + os.waitpid(pids[0], 0) + # This is the same code rsync uses for timeout. + exitcode = 30 + else: + if exitcode != os.EX_OK: + if exitcode & 0xff: + exitcode = (exitcode & 0xff) << 8 + else: + exitcode = exitcode >> 8 + + if content: + try: + servertimestamp = time.mktime(time.strptime( + content[0], TIMESTAMP_FORMAT)) + except (OverflowError, ValueError): + pass + del command, pids, content + + if exitcode == os.EX_OK: + if (servertimestamp != 0) and (servertimestamp == timestamp): + self.logger(self.xterm_titles, + ">>> Cancelling sync -- Already current.") + print() + print(">>>") + print(">>> Timestamps on the server and in the local repository are the same.") + print(">>> Cancelling all further sync action. You are already up to date.") + print(">>>") + print(">>> In order to force sync, remove '%s'." % self.servertimestampfile) + print(">>>") + print() + return is_synced, exitcode + elif (servertimestamp != 0) and (servertimestamp < timestamp): + self.logger(self.xterm_titles, + ">>> Server out of date: %s" % syncuri) + print() + print(">>>") + print(">>> SERVER OUT OF DATE: %s" % syncuri) + print(">>>") + print(">>> In order to force sync, remove '%s'." % self.servertimestampfile) + print(">>>") + print() + exitcode = SERVER_OUT_OF_DATE + elif (servertimestamp == 0) or (servertimestamp > timestamp): + # actual sync + command = rsynccommand + [syncuri+"/", self.repo.location] + exitcode = None + try: + exitcode = portage.process.spawn(command, + **portage._native_kwargs(self.spawn_kwargs)) + finally: + if exitcode is None: + # interrupted + exitcode = 128 + signal.SIGINT + + # 0 Success + # 1 Syntax or usage error + # 2 Protocol incompatibility + # 5 Error starting client-server protocol + # 35 Timeout waiting for daemon connection + if exitcode not in (0, 1, 2, 5, 35): + # If the exit code is not among those listed above, + # then we may have a partial/inconsistent sync + # state, so our previously read timestamp as well + # as the corresponding file can no longer be + # trusted. + timestamp = 0 + try: + os.unlink(self.servertimestampfile) + except OSError: + pass + + if exitcode in [0,1,3,4,11,14,20,21]: + is_synced = True + elif exitcode in [1,3,4,11,14,20,21]: + is_synced = True + else: + # Code 2 indicates protocol incompatibility, which is expected + # for servers with protocol < 29 that don't support + # --prune-empty-directories. Retry for a server that supports + # at least rsync protocol version 29 (>=rsync-2.6.4). + pass + return is_synced, exitcode diff --git a/pym/portage/sync/modules/svn/__init__.py b/pym/portage/sync/modules/svn/__init__.py new file mode 100644 index 0000000..1fda55a --- /dev/null +++ b/pym/portage/sync/modules/svn/__init__.py @@ -0,0 +1,32 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +"""SVN plug-in module for portage. +Performs a svn up on repositories +""" + + +from portage.localization import _ +from portage.sync.config_checks import CheckSyncConfig +from portage.util import writemsg_level + + +module_spec = { + 'name': 'svn', + 'description': __doc__, + 'provides':{ + 'svn-module': { + 'name': "svn", + 'class': "SVNSync", + 'description': __doc__, + 'functions': ['sync', 'new', 'exists'], + 'func_desc': { + 'sync': 'Performs a svn up on the repository', + 'new': 'Creates the new repository at the specified location', + 'exists': 'Returns a boolean of whether the specified dir ' + + 'exists and is a valid SVN repository', + }, + 'validate_config': CheckSyncConfig, + } + } +} diff --git a/pym/portage/sync/modules/svn/svn.py b/pym/portage/sync/modules/svn/svn.py new file mode 100644 index 0000000..0365e90 --- /dev/null +++ b/pym/portage/sync/modules/svn/svn.py @@ -0,0 +1,104 @@ +# Copyright 1999-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import logging +import errno + +import portage +from portage import os +from portage.util import writemsg_level +from portage.sync.syncbase import SyncBase + + +class SVNSync(SyncBase): + '''SVN sync module''' + + short_desc = "Perform sync operations on SVN repositories" + + @staticmethod + def name(): + return "SVNSync" + + + def __init__(self): + SyncBase.__init__(self, "svn", "dev-vcs/subversion") + + + def new(self, **kwargs): + if kwargs: + self._kwargs(kwargs) + #initial checkout + msg = ">>> Starting initial svn checkout with %s..." % self.repo.sync_uri + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + try: + os.rmdir(self.repo.location) + except OSError as e: + if e.errno != errno.ENOENT: + msg = "!!! existing '%s' directory; exiting." % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (1, False) + del e + svn_root = self.repo.sync_uri + exitcode = portage.process.spawn_bash( + "cd %s; exec svn %s" % + (portage._shell_quote(os.path.dirname(self.repo.location)), + portage._shell_quote(svn_root)), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! svn checkout error; exiting." + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (exitcode, False) + + + def _sync(self): + """ + Internal function to sync an existing SVN repository + + @return: tuple of return code (0=success), whether the cache + needs to be updated + @rtype: (int, bool) + """ + + exitcode, d = self._svn_upgrade() + if exitcode != os.EX_OK: + return (exitcode, False) + + svn_root = self.repo.sync_uri + + if svn_root.startswith("svn://"): + svn_root = svn_root[6:] + #svn update + msg = ">>> Starting svn update with %s..." % self.repo.sync_uri + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + exitcode = portage.process.spawn_bash( + "cd %s; exec svn update" % \ + (portage._shell_quote(self.repo.location),), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! svn update error; exiting." + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (exitcode, False) + + + def _svn_upgrade(self): + """ + Internal function which performs an svn upgrade on the repo + + @return: tuple of return code (0=success), whether the cache + needs to be updated + @rtype: (int, bool) + """ + exitcode = portage.process.spawn_bash( + "cd %s; exec svn upgrade" % + (portage._shell_quote(self.repo.location),), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! svn upgrade error; exiting." + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", noiselevel=-1, level=logging.ERROR) + return (exitcode, False) diff --git a/pym/portage/sync/modules/websync/__init__.py b/pym/portage/sync/modules/websync/__init__.py new file mode 100644 index 0000000..e93ee10 --- /dev/null +++ b/pym/portage/sync/modules/websync/__init__.py @@ -0,0 +1,48 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +"""WebRSync plug-in module for portage. +Performs a http download of a portage snapshot, verifies and +unpacks it to the repo location. +""" + +import os + +from portage.sync.config_checks import CheckSyncConfig + + +DEFAULT_CLASS = "WebRsync" +AVAILABLE_CLASSES = [ "WebRsync", "PyWebsync"] +options = {"1": "WebRsync", "2": "PyWebsync"} + + +config_class = DEFAULT_CLASS +try: + test_param = os.environ["TESTIT"] + if test_param in options: + config_class = options[test_param] +except KeyError: + pass + + +module_spec = { + 'name': 'webrsync', + 'description': __doc__, + 'provides':{ + 'websync-module': { + 'name': "websync", + 'class': config_class, + 'description': __doc__, + 'functions': ['sync', 'new', 'exists'], + 'func_desc': { + 'sync': 'Performs an archived http download of the ' + + 'repository, then unpacks it. Optionally it performs a ' + + 'gpg verification of the downloaded file(s)', + 'new': 'Creates the new repository at the specified location', + 'exists': 'Returns a boolean of whether the specified dir ' + + 'exists and is a valid repository', + }, + 'validate_config': CheckSyncConfig, + }, + } +} diff --git a/pym/portage/sync/modules/websync/websync.py b/pym/portage/sync/modules/websync/websync.py new file mode 100644 index 0000000..f08ae77 --- /dev/null +++ b/pym/portage/sync/modules/websync/websync.py @@ -0,0 +1,81 @@ + +'''WebRsync module for portage''' + +import logging + +import portage +from portage import os +from portage.util import writemsg_level +from portage.output import create_color_func +good = create_color_func("GOOD") +bad = create_color_func("BAD") +warn = create_color_func("WARN") +from portage.sync.syncbase import SyncBase + + +class WebRsync(SyncBase): + '''WebRSync sync class''' + + short_desc = "Perform sync operations on webrsync based repositories" + + @staticmethod + def name(): + return "WebRSync" + + + def __init__(self): + SyncBase.__init__(self, 'emerge-webrsync', '>=sys-apps/portage-2.3') + + + def new(self, **kwargs): + '''Do the initial download and install of the repository''' + return self._sync() + + + def _sync(self): + ''' Update existing repository + ''' + emerge_config = self.options.get('emerge_config', None) + portdb = self.options.get('portdb', None) + + msg = ">>> Starting emerge-webrsync for %s..." % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + exitcode = portage.process.spawn_bash("%s" % \ + (self.bin_command), + **portage._native_kwargs(self.spawn_kwargs)) + if exitcode != os.EX_OK: + msg = "!!! emerge-webrsync error in %s" % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n", level=logging.ERROR, noiselevel=-1) + return (exitcode, False) + msg = ">>> Emerge-webrsync successful: %s" % self.repo.location + self.logger(self.xterm_titles, msg) + writemsg_level(msg + "\n") + #return self.post_sync(portdb, self.repo.location, emerge_config) + return (exitcode, True) + + +class PyWebRsync(SyncBase): + '''WebRSync sync class''' + + short_desc = "Perform sync operations on webrsync based repositories" + + @staticmethod + def name(): + return "WebRSync" + + + def __init__(self): + SyncBase.__init__(self, None, '>=sys-apps/portage-2.3') + + + def new(self, **kwargs): + '''Do the initial download and install of the repository''' + pass + + + def _sync(self): + ''' Update existing repository + ''' + pass diff --git a/pym/portage/sync/old_tree_timestamp.py b/pym/portage/sync/old_tree_timestamp.py new file mode 100644 index 0000000..aa23a27 --- /dev/null +++ b/pym/portage/sync/old_tree_timestamp.py @@ -0,0 +1,100 @@ +# Copyright 2010-2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from __future__ import division + +import locale +import logging +import time + +from portage import os +from portage.exception import PortageException +from portage.localization import _ +from portage.output import EOutput +from portage.util import grabfile, writemsg_level + +def have_english_locale(): + lang, enc = locale.getdefaultlocale() + if lang is not None: + lang = lang.lower() + lang = lang.split('_', 1)[0] + return lang is None or lang in ('c', 'en') + +def whenago(seconds): + sec = int(seconds) + mins = 0 + days = 0 + hrs = 0 + years = 0 + out = [] + + if sec > 60: + mins = sec // 60 + sec = sec % 60 + if mins > 60: + hrs = mins // 60 + mins = mins % 60 + if hrs > 24: + days = hrs // 24 + hrs = hrs % 24 + if days > 365: + years = days // 365 + days = days % 365 + + if years: + out.append("%dy " % years) + if days: + out.append("%dd " % days) + if hrs: + out.append("%dh " % hrs) + if mins: + out.append("%dm " % mins) + if sec: + out.append("%ds " % sec) + + return "".join(out).strip() + +def old_tree_timestamp_warn(portdir, settings): + unixtime = time.time() + default_warnsync = 30 + + timestamp_file = os.path.join(portdir, "metadata/timestamp.x") + try: + lastsync = grabfile(timestamp_file) + except PortageException: + return False + + if not lastsync: + return False + + lastsync = lastsync[0].split() + if not lastsync: + return False + + try: + lastsync = int(lastsync[0]) + except ValueError: + return False + + var_name = 'PORTAGE_SYNC_STALE' + try: + warnsync = float(settings.get(var_name, default_warnsync)) + except ValueError: + writemsg_level("!!! %s contains non-numeric value: %s\n" % \ + (var_name, settings[var_name]), + level=logging.ERROR, noiselevel=-1) + return False + + if warnsync <= 0: + return False + + if (unixtime - 86400 * warnsync) > lastsync: + out = EOutput() + if have_english_locale(): + out.ewarn("Last emerge --sync was %s ago." % \ + whenago(unixtime - lastsync)) + else: + out.ewarn(_("Last emerge --sync was %s.") % \ + time.strftime('%c', time.localtime(lastsync))) + return True + return False diff --git a/pym/portage/sync/syncbase.py b/pym/portage/sync/syncbase.py new file mode 100644 index 0000000..94d4aab --- /dev/null +++ b/pym/portage/sync/syncbase.py @@ -0,0 +1,102 @@ +# Copyright 2014 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +''' +Base class for performing sync operations. +This class contains common initialization code and functions. +''' + + +import os + +import portage +from portage.util import writemsg_level + + +class SyncBase(object): + '''Base Sync class for subclassing''' + + short_desc = "Perform sync operations on repositories" + + @staticmethod + def name(): + return "BlankSync" + + + def can_progressbar(self, func): + return False + + + def __init__(self, bin_command, bin_pkg): + self.options = None + self.settings = None + self.logger = None + self.repo = None + self.xterm_titles = None + self.spawn_kwargs = None + self.bin_command = None + self.has_bin = False + if bin_command: + self.bin_command = portage.process.find_binary(bin_command) + if self.bin_command is None: + msg = ["Command not found: %s" % bin_command, + "Type \"emerge %s\" to enable %s support." % (bin_pkg, bin_command)] + for l in msg: + writemsg_level("!!! %s\n" % l, + level=self.logger.ERROR, noiselevel=-1) + else: + self.has_bin = True + + + def _kwargs(self, kwargs): + '''Sets internal variables from kwargs''' + self.options = kwargs.get('options', {}) + self.settings = self.options.get('settings', None) + self.logger = self.options.get('logger', None) + self.repo = self.options.get('repo', None) + self.xterm_titles = self.options.get('xterm_titles', False) + self.spawn_kwargs = self.options.get('spawn_kwargs', None) + + + def exists(self, **kwargs): + '''Tests whether the repo actually exists''' + if kwargs: + self._kwargs(kwargs) + elif not self.repo: + return False + + + if not os.path.exists(self.repo.location): + return False + return True + + + def sync(self, **kwargs): + '''Sync the repository''' + if kwargs: + self._kwargs(kwargs) + + if not self.has_bin: + return (1, False) + + if not self.exists(): + return self.new() + return self._sync() + + + def new(self, **kwargs): + '''Do the initial download and install of the repository''' + pass + + def _sync(self): + '''Update existing repository + ''' + pass + + def post_sync(self, portdb, location, emerge_config): + '''repo.sync_type == "Blank": + # NOTE: Do this after reloading the config, in case + # it did not exist prior to sync, so that the config + # and portdb properly account for its existence. + ''' + pass