示例#1
0
 def symlinks_equal(sym1, sym2):
     return normpath(sym1["name"]) == normpath(sym2["name"]) and \
            normpath(sym1["target"]) == normpath(sym2["target"]) and \
            sym1["uid"] == sym2["uid"] and \
            sym1["gid"] == sym2["gid"] and \
            sym1["permission"] == sym2["permission"] and \
            sym1["secure"] == sym2["secure"]
示例#2
0
    def add_source(self, target):
        """Adds a source path and normalizes it.

        Args:
            target (str): A path to a file that will be used as source
        """
        self.sources.append(normpath(target))
示例#3
0
    def __setattr__(self, name, value):
        """Setter for :attr:`self.directory<Profile.directory>`.

        Makes sure that :attr:`self.directory<Profile.directory>` is always
        expanded and noramlized.
        """
        if name == "directory":
            if hasattr(self, name):
                value = os.path.join(self.directory, expandpath(value))
            else:
                value = normpath(value)
        super(Profile, self).__setattr__(name, value)
示例#4
0
    def getdir(self):
        """Gets the path of the directory that is used to store the generated
        file.

        Returns:
            str: The path to the directory
        """
        path = normpath(os.path.join(constants.DATA_DIR, self.SUBDIR))
        # Create dir if it doesn't exist
        if not os.path.isdir(path):
            log_debug("Creating directory '" + path + "'")
            os.mkdir(path)
        return path
示例#5
0
 def symlinks_similar(sym1, sym2):
     return normpath(sym1["name"]) == normpath(sym2["name"]) or \
            normpath(sym1["target"]) == normpath(sym2["target"])
示例#6
0
    def __create_link_descriptor(self, target, directory="", **kwargs):
        """Creates an entry in ``self.result["links"]`` with current options
        and a given target.

        Furthermore lets you set the directory like :func:`cd()`.

        Args:
            target (str): Full path to target file
            directory (str): A path to change the cwd
            kwargs (dict): A set of options that will be overwritten just for
                this call
        Raises:
            :class:`~errors.GenerationError`: One or more options were misused
        """
        read_opt = self._make_read_opt(kwargs)

        # First generate the correct name for the symlink
        replace = read_opt("replace")
        name = read_opt("name")
        if replace:  # When using regex pattern, name property is ignored
            replace_pattern = read_opt("replace_pattern")
            if not replace_pattern:
                msg = "You are trying to use 'replace', but no "
                msg += "'replace_pattern' was set."
                self._gen_err(msg)
            if name:
                # Usually it makes no sense to set a name when "replace" is
                # used, but commands might set this if they got an
                # dynamicfile, because otherwise you would have to match
                # against the hash too
                base = name
            else:
                base = os.path.basename(target)
            if constants.TAG_SEPARATOR in base:
                base = base.split(constants.TAG_SEPARATOR, 1)[1]
            name = re.sub(replace_pattern, replace, base)
        elif name:
            name = expandpath(name)
            # And prevent exceptions in os.symlink()
            if name[-1:] == "/":
                self._gen_err("name mustn't represent a directory")
        else:
            # "name" wasn't set by the user,
            # so fallback to use the target name (but without the tag)
            name = os.path.basename(
                target.split(constants.TAG_SEPARATOR, 1)[-1])

        # Add prefix an suffix to name
        base, ext = os.path.splitext(os.path.basename(name))
        if read_opt("extension"):
            ext = "." + read_opt("extension")
        name = os.path.join(
            os.path.dirname(name),
            read_opt("prefix") + base + read_opt("suffix") + ext)

        # Put together the path of the dir we create the link in
        if not directory:
            directory = self.directory  # Use the current dir
        else:
            directory = os.path.join(self.directory, expandpath(directory))
        # Concat directory and name. The users $HOME needs to be set for this
        # when executing as root, otherwise ~ will be expanded to the home
        # directory of the root user (/root)
        name = normpath(os.path.join(directory, name))

        # Get user and group id of owner
        owner = read_opt("owner")
        if owner:
            # Check for the correct format of owner
            try:
                user, group = owner.split(":")
            except ValueError:
                msg = "The owner needs to be specified in the format "
                self._gen_err(msg + "user:group")
            try:
                uid = shutil._get_uid(user)
            except LookupError:
                msg = "You want to set the owner of '" + name + "' to '" + user
                msg += "', but there is no such user on this system."
                self._gen_err(msg)
            try:
                gid = shutil._get_gid(group)
            except LookupError:
                msg = "You want to set the owner of '" + name + "' to '"
                msg += group + "', but there is no such group on this system."
                self._gen_err(msg)
        else:
            # if no owner was specified, we need to set it
            # to the owner of the dir
            uid, gid = get_dir_owner(name)

        # Finally create the result entry
        linkdescriptor = {}
        linkdescriptor["target"] = target
        linkdescriptor["name"] = name
        linkdescriptor["uid"] = uid
        linkdescriptor["gid"] = gid
        linkdescriptor["permission"] = read_opt("permission")
        linkdescriptor["secure"] = read_opt("secure")
        self.result["links"].append(linkdescriptor)
示例#7
0
    def parse_arguments(self, arguments=None):
        """Parses the commandline arguments. This function can parse a custom
        list of arguments, instead of ``sys.args``.

        Args:
            arguments (list): A list of arguments that will be parsed instead
                of ``sys.args``

        Raises:
            :class:`~errors.UserError`: One ore more arguments are invalid or
                used in an invalid combination.
        """
        if arguments is None:
            arguments = sys.argv[1:]
        # Setup parser
        parser = CustomParser()
        # Options
        parser.add_argument("--config",
                            help="specify another config-file to use")
        parser.add_argument("--directory", help="set the default directory")
        parser.add_argument("-d",
                            "--dryrun",
                            help="just simulate what would happen",
                            action="store_true")
        parser.add_argument("--dui",
                            help="use the DUI strategy for updating links",
                            action="store_true")
        parser.add_argument("-f",
                            "--force",
                            help="overwrite existing files with links",
                            action="store_true")
        parser.add_argument("--info",
                            help="print everything but debug messages",
                            action="store_true")
        parser.add_argument("--log", help="specify a file to log to")
        parser.add_argument("-m",
                            "--makedirs",
                            help="create directories automatically if needed",
                            action="store_true")
        parser.add_argument("--option",
                            help="set options for profiles",
                            dest="opt_dict",
                            action=StoreDictKeyPair,
                            nargs="+",
                            metavar="KEY=VAL")
        parser.add_argument("--parent",
                            help="set the parent of the profiles you install")
        parser.add_argument("--plain",
                            help="print the internal DiffLog as plain json",
                            action="store_true")
        parser.add_argument("-p",
                            "--print",
                            help="print what changes uberdot will do",
                            action="store_true")
        parser.add_argument("-q",
                            "--quiet",
                            help="print nothing but errors",
                            action="store_true")
        parser.add_argument("--save",
                            help="specify another install-file to use",
                            default="default")
        parser.add_argument("--silent",
                            help="print absolute nothing",
                            action="store_true")
        parser.add_argument("--skipafter",
                            help="do not execute events after linking",
                            action="store_true")
        parser.add_argument("--skipbefore",
                            help="do not execute events before linking",
                            action="store_true")
        parser.add_argument("--skipevents",
                            help="do not execute any events",
                            action="store_true")
        parser.add_argument("--skiproot",
                            help="do nothing that requires root permissions",
                            action="store_true")
        parser.add_argument("--superforce",
                            help="overwrite blacklisted/protected files",
                            action="store_true")
        parser.add_argument("-v",
                            "--verbose",
                            help="print stacktrace in case of error",
                            action="store_true")
        # Modes
        modes = parser.add_mutually_exclusive_group(required=True)
        modes.add_argument("-h",
                           "--help",
                           help="show this help message and exit",
                           action="help")
        modes.add_argument("-i",
                           "--install",
                           help="install and update (sub)profiles",
                           action="store_true")
        modes.add_argument("--debuginfo",
                           help="display internal values",
                           action="store_true")
        modes.add_argument("-u",
                           "--uninstall",
                           help="uninstall (sub)profiles",
                           action="store_true")
        modes.add_argument("-s",
                           "--show",
                           help="show infos about installed profiles",
                           action="store_true")
        modes.add_argument("--version",
                           help="print version number",
                           action="store_true")
        # Profile list
        parser.add_argument("profiles",
                            help="list of root profiles",
                            nargs="*")

        # Read arguments
        try:
            self.args = parser.parse_args(arguments)
        except argparse.ArgumentError as err:
            raise UserError(err.message)

        # Options need some extra parsing for tags
        if self.args.opt_dict and "tags" in self.args.opt_dict:
            reader = csv.reader([self.args.opt_dict["tags"]])
            self.args.opt_dict["tags"] = next(reader)
        # And the directory was specified relative to the old working directory
        if self.args.directory:
            self.args.directory = os.path.join(self.owd, self.args.directory)
        # Like the path to the config file
        if self.args.config:
            self.args.config = os.path.join(self.owd, self.args.config)

        # Load constants for this installed-file
        constants.loadconfig(self.args.config, self.args.save)
        # Write back defaults from config for arguments that weren't set
        if not self.args.dui:
            self.args.dui = constants.DUISTRATEGY
        if not self.args.force:
            self.args.force = constants.FORCE
        if not self.args.makedirs:
            self.args.makedirs = constants.MAKEDIRS
        if not self.args.skipafter:
            self.args.skipafter = constants.SKIPAFTER
        if not self.args.skipbefore:
            self.args.skipbefore = constants.SKIPBEFORE
        if not self.args.skipevents:
            self.args.skipevents = constants.SKIPEVENTS
        if not self.args.skiproot:
            self.args.skiproot = constants.SKIPROOT
        if not self.args.superforce:
            self.args.superforce = constants.SUPERFORCE
        if not self.args.directory:
            self.args.directory = constants.DIR_DEFAULT
        if not self.args.opt_dict:
            self.args.opt_dict = constants.DEFAULTS
        if not self.args.log and constants.LOGFILE:
            self.args.log = normpath(constants.LOGFILE)
        else:
            # Merge options provided by commandline with loaded defaults
            tmptags = self.args.opt_dict["tags"] + constants.DEFAULTS["tags"]
            self.args.opt_dict = {**constants.DEFAULTS, **self.args.opt_dict}
            self.args.opt_dict["tags"] = tmptags

        # Configure logger
        if self.args.silent:
            constants.LOGGINGLEVEL = "SILENT"
        if self.args.quiet:
            constants.LOGGINGLEVEL = "QUIET"
        if self.args.info:
            constants.LOGGINGLEVEL = "INFO"
        if self.args.verbose:
            constants.LOGGINGLEVEL = "VERBOSE"
        logging_level_mapping = {
            "SILENT": logging.CRITICAL,
            "QUIET": logging.WARNING,
            "INFO": logging.INFO,
            "VERBOSE": logging.DEBUG
        }
        try:
            logger.setLevel(logging_level_mapping[constants.LOGGINGLEVEL])
        except KeyError:
            msg = "Unkown logginglevel '" + constants.LOGGINGLEVEL + "'"
            raise UserError(msg)
        if self.args.log:
            ch = logging.FileHandler(os.path.join(self.owd, self.args.log))
            ch.setLevel(logging.DEBUG)
            form = '[%(asctime)s] - %(levelname)s - %(message)s'
            formatter = logging.Formatter(form)
            ch.setFormatter(formatter)
            logger.addHandler(ch)
示例#8
0
def loadconfig(config_file, installed_filename="default"):
    """Loads constants from the config files.

    This will load all configs from :const:`CFG_FILES` or ``config_file``
    if provided. The name of the installed-file will be used to load
    installed-file specific values.

    Args:
        config_file (str): Absolute path to the config file to use. If None,
            the configs from :const:`CFG_FILES` will be loaded.
        installed_filename (str): Name of the installed-file for that values
            will be loaded
    """
    global C_OK, C_WARNING, C_FAIL, ENDC, BOLD, C_HIGHLIGHT, NOBOLD, C_DEBUG
    global DUISTRATEGY, FORCE, LOGGINGLEVEL, MAKEDIRS, DECRYPT_PWD, SUPERFORCE
    global SKIPROOT, DATA_DIR, SHELL, SHELL_TIMEOUT, SMART_CD, SKIPEVENTS
    global BACKUP_EXTENSION, PROFILE_FILES, TARGET_FILES, INSTALLED_FILE_BACKUP
    global COLOR, INSTALLED_FILE, DEFAULTS, DIR_DEFAULT, LOGFILE, CFG_FILES
    global ASKROOT, TAG_SEPARATOR, HASH_SEPARATOR, SKIPAFTER, SKIPBEFORE
    global SHELL_ARGS

    # Load config files
    if config_file:
        CFG_FILES = [config_file]
    else:
        CFG_FILES = find_files("uberdot.ini", CONFIG_SEARCH_PATHS)
    config = configparser.ConfigParser()
    try:
        for cfg in CFG_FILES:
            config.read(cfg)
            # We need to normalize all paths here, relatively to
            # the config file which it defined
            path_keys = [
                "directory", "profilefiles", "targetfiles", "logfile",
                "datadir"
            ]
            for section in config.sections():
                for item in config.items(section):
                    key = item[0]
                    if key in path_keys:
                        config[section][key] = os.path.join(
                            os.path.dirname(cfg), config[section][key])
                        config[section][key] = os.path.normpath(
                            config[section][key])
    except configparser.Error as err:
        msg = "Can't parse config at '" + cfg + "'. " + err.message
        raise PreconditionError(msg)

    # Setup special lookup function for getting values
    def getvalue(getter, section):
        """Creates function to lookup a specific value in a specific section
        with a specific getter.

        Args:
            getter (function): getter function to perform a single lookup
            section (str): The section that contains the key
        Returns:
            function: A function that can lookup keys in the config
        """
        def lookup(key, fallback=None):
            """Looks up a value in a specific section for a specific type.

            Args:
                key (str): The name of the value that will be looked up
                fallback: A fallback value
            Returns:
                The value of the key
            """
            installedfile_section = "Installed." + installed_filename
            installedfile_section += "." + section
            value = getter(section, key, fallback=fallback)
            return getter(installedfile_section, key, fallback=value)

        return lookup

    # Get arguments
    getstr = getvalue(config.get, "Arguments")
    getbool = getvalue(config.getboolean, "Arguments")
    DUISTRATEGY = getbool("dui", DUISTRATEGY)
    FORCE = getbool("force", FORCE)
    MAKEDIRS = getbool("makedirs", MAKEDIRS)
    SKIPAFTER = getbool("skipafter", SKIPAFTER)
    SKIPBEFORE = getbool("skipbefore", SKIPBEFORE)
    SKIPEVENTS = getbool("skipevents", SKIPEVENTS)
    SKIPROOT = getbool("skiproot", SKIPROOT)
    SUPERFORCE = getbool("superforce", SUPERFORCE)
    LOGGINGLEVEL = getstr("logginglevel", LOGGINGLEVEL).upper()
    LOGFILE = getstr("logfile", LOGFILE)

    # Get settings
    getstr = getvalue(config.get, "Settings")
    getbool = getvalue(config.getboolean, "Settings")
    getint = getvalue(config.getint, "Settings")
    ASKROOT = getbool("askroot", ASKROOT)
    SHELL = getstr("shell", SHELL)
    SHELL_ARGS = getstr("shellArgs", SHELL_ARGS)
    SHELL_TIMEOUT = getint("shellTimeout", SHELL_TIMEOUT)
    DECRYPT_PWD = getstr("decryptPwd", DECRYPT_PWD)
    BACKUP_EXTENSION = getstr("backupExtension", BACKUP_EXTENSION)
    TAG_SEPARATOR = getstr("tagSeparator", TAG_SEPARATOR)
    HASH_SEPARATOR = getstr("hashSeparator", HASH_SEPARATOR)
    PROFILE_FILES = getstr("profileFiles")
    TARGET_FILES = getstr("targetFiles")
    DATA_DIR = normpath(getstr("dataDir", DATA_DIR))
    COLOR = getbool("color", COLOR)
    SMART_CD = getbool("smartShellCWD", SMART_CD)

    # Setup internal values
    INSTALLED_FILE = os.path.join(DATA_DIR, "installed/%s.json")
    INSTALLED_FILE_BACKUP = INSTALLED_FILE + "." + BACKUP_EXTENSION
    if not COLOR:
        C_OK = C_WARNING = C_FAIL = ENDC = BOLD = C_HIGHLIGHT = NOBOLD = ''
        C_DEBUG = ''

    # Get command options
    getstr = getvalue(config.get, "Defaults")
    getbool = getvalue(config.getboolean, "Defaults")
    getint = getvalue(config.getint, "Defaults")
    DEFAULTS = {
        "extension": getstr("extension", DEFAULTS["extension"]),
        "name": getstr("name", DEFAULTS["name"]),
        "optional": getbool("optional", DEFAULTS["optional"]),
        "owner": getstr("owner", DEFAULTS["owner"]),
        "permission": getint("permission", DEFAULTS["permission"]),
        "prefix": getstr("prefix", DEFAULTS["prefix"]),
        "replace": getstr("replace", DEFAULTS["replace"]),
        "replace_pattern": getstr("replace_pattern",
                                  DEFAULTS["replace_pattern"]),
        "suffix": getstr("suffix", DEFAULTS["suffix"]),
        "secure": getbool("secure", DEFAULTS["secure"]),
        "tags": next(csv.reader([getstr("tags", "")]))
    }
    DIR_DEFAULT = getstr("directory", DIR_DEFAULT)

    # Insert installed-file into constants
    INSTALLED_FILE = INSTALLED_FILE % installed_filename
    INSTALLED_FILE_BACKUP = INSTALLED_FILE_BACKUP % installed_filename

    # Check if TARGET_FILES and PROFILE_FILES were set by the user
    if not TARGET_FILES or TARGET_FILES == "</path/to/your/dotfiles/>":
        raise UserError("No directory for your dotfiles specified.")
    if not PROFILE_FILES or PROFILE_FILES == "</path/to/your/profiles/>":
        raise UserError("No directory for your profiles specified.")
示例#9
0
NOBOLD = '\033[22m'
"""Bash color code to stop bold text."""

# Loaders for config and installed-section
###############################################################################

# Search paths for config files
CFG_FILES = []
"""A list with all configuration files that will be used to set constants. All
settings of all configuration files will be used. If a specific setting is set
in more than one configuration file, the setting from the configuration file
with higher index will be prefered.
"""
CONFIG_SEARCH_PATHS = [
    "/etc/uberdot",
    os.path.join(get_user_env_var('XDG_CONFIG_HOME', normpath('~/.config')),
                 "uberdot"), DATA_DIR
]
"""A list of paths that will be used to search for configuration files. """


def loadconfig(config_file, installed_filename="default"):
    """Loads constants from the config files.

    This will load all configs from :const:`CFG_FILES` or ``config_file``
    if provided. The name of the installed-file will be used to load
    installed-file specific values.

    Args:
        config_file (str): Absolute path to the config file to use. If None,
            the configs from :const:`CFG_FILES` will be loaded.