diff --git a/README b/README index 96388a5..998ffe5 100644 --- a/README +++ b/README @@ -107,34 +107,83 @@ CLASSES Config class AgentManager(__builtin__.object) + | Manages the ssh-agent for one identity. + | | Methods defined here: | | FindUnloadedKeys(self, keys) + | Determines which keys have not been loaded yet. + | + | Args: + | keys: dict as returned by FindKeys. + | + | Returns: + | iterable of strings, paths to private key files to load. | | GetLoadedKeys(self) + | Returns an iterable of strings, each the fingerprint of a loaded key. | | LoadKeyFiles(self, keys) + | Load all specified keys. + | + | Args: + | keys: iterable of strings, each string a path to a key to load. | | LoadUnloadedKeys(self, keys) + | Loads all the keys specified that are not loaded. + | + | Args: + | keys: dict as returned by FindKeys. | | RunSSH(self, argv) + | Execs ssh with the specified arguments. | | __init__(self, identity, config) + | Initializes an AgentManager object. + | + | Args: + | identity: string, identity the ssh-agent managed by this instance of + | an AgentManager will control. + | config: object implementing the Config interface, allows access to + | the user configuration parameters. + | + | Attributes: + | identity: same as above. + | config: same as above. + | agents_path: directory where the config of all agents is kept. + | agent_file: the config of the agent corresponding to this identity. + | + | Parameters: + | DIR_AGENTS: used to compute agents_path. + | BINARY_SSH: path to the ssh binary. | | ---------------------------------------------------------------------- | Static methods defined here: | | EscapeShellArguments(argv) + | Escapes all arguments to the shell, returns a string. | | GetAgentFile(path, identity) + | Returns the path to an agent config file. + | + | Args: + | path: string, the path where agent config files are kept. + | identity: string, identity for which to load the agent. + | + | Returns: + | string, path to the agent file. | | GetPublicKeyFingerprint(key) + | Returns the fingerprint of a public key as a string. | | IsAgentFileValid(agentfile) + | Returns true if the specified agentfile refers to a running agent. | | RunShellCommand(command) + | Runs a shell command, returns (status, stdout), (int, string). | | RunShellCommandInAgent(agentfile, command) + | Runs a shell command with an agent configured in the environment. | | ---------------------------------------------------------------------- | Data descriptors defined here: @@ -146,11 +195,15 @@ CLASSES | list of weak references to the object (if defined) class Config(__builtin__.object) + | Holds and loads users configurations. + | | Methods defined here: | | Get(self, parameter) + | Returns the value of a parameter, or causes the script to exit. | | Load(self) + | Load configurations from the default user file. | | __init__(self) | @@ -158,6 +211,7 @@ CLASSES | Static methods defined here: | | Expand(value) + | Expand environment variables or ~ in string parameters. | | ---------------------------------------------------------------------- | Data descriptors defined here: @@ -198,6 +252,21 @@ FUNCTIONS matching the first element in elements. FindKeys(identity, config) + Finds all the private and public keys associated with an identity. + + Args: + identity: string, name of the identity to load strings of. + config: object implementing the Config interface, providing configurations + for the user. + + Returns: + dict, {"key name": {"pub": "/path/to/public/key", "priv": + "/path/to/private/key"}}, for each key found, the path of the public + key and private key. The key name is just a string representing the + key. Note that for a given key, it is not guaranteed that both the + public and private key will be found. + The return value is affected by DIR_IDENTITIES and PATTERN_KEYS + configuration parameters. main(argv) diff --git a/ssh b/ssh index 502481d..716a2e3 100755 --- a/ssh +++ b/ssh @@ -107,6 +107,8 @@ import collections class Config(object): + """Holds and loads users configurations.""" + defaults = { # Where to find the per-user configuration. "FILE_USER_CONFIG": "$HOME/.ssh-ident", @@ -135,23 +137,28 @@ class Config(object): self.values = {} def Load(self): + """Load configurations from the default user file.""" path = self.Get("FILE_USER_CONFIG") variables = {} try: execfile(path, {}, variables) except IOError: - print >>sys.stderr, "Warning: could not load config '%s', you might as well be using plain ssh." % path + print >>sys.stderr, ( + "Warning: could not load config '%s', " + "you might as well be using plain ssh." % path) return self self.values = variables return self @staticmethod def Expand(value): + """Expand environment variables or ~ in string parameters.""" if isinstance(value, str): return os.path.expanduser(os.path.expandvars(value)) return value def Get(self, parameter): + """Returns the value of a parameter, or causes the script to exit.""" if parameter in self.values: return self.Expand(self.values[parameter]) if parameter in self.defaults: @@ -198,6 +205,22 @@ def FindIdentity(argv, config): config.Get("DEFAULT_IDENTITY")) def FindKeys(identity, config): + """Finds all the private and public keys associated with an identity. + + Args: + identity: string, name of the identity to load strings of. + config: object implementing the Config interface, providing configurations + for the user. + + Returns: + dict, {"key name": {"pub": "/path/to/public/key", "priv": + "/path/to/private/key"}}, for each key found, the path of the public + key and private key. The key name is just a string representing the + key. Note that for a given key, it is not guaranteed that both the + public and private key will be found. + The return value is affected by DIR_IDENTITIES and PATTERN_KEYS + configuration parameters. + """ keyfiles = glob.glob(os.path.join( config.Get("DIR_IDENTITIES"), identity, config.Get("PATTERN_KEYS"))) @@ -218,13 +241,38 @@ def FindKeys(identity, config): return found class AgentManager(object): + """Manages the ssh-agent for one identity.""" + def __init__(self, identity, config): + """Initializes an AgentManager object. + + Args: + identity: string, identity the ssh-agent managed by this instance of + an AgentManager will control. + config: object implementing the Config interface, allows access to + the user configuration parameters. + + Attributes: + identity: same as above. + config: same as above. + agents_path: directory where the config of all agents is kept. + agent_file: the config of the agent corresponding to this identity. + + Parameters: + DIR_AGENTS: used to compute agents_path. + BINARY_SSH: path to the ssh binary. + """ self.identity = identity self.config = config self.agents_path = os.path.abspath(config.Get("DIR_AGENTS")) self.agent_file = self.GetAgentFile(self.agents_path, self.identity) def LoadUnloadedKeys(self, keys): + """Loads all the keys specified that are not loaded. + + Args: + keys: dict as returned by FindKeys. + """ toload = self.FindUnloadedKeys(keys) if toload: print "Loading keys:\n %s" % "\n ".join(toload) @@ -233,6 +281,14 @@ class AgentManager(object): print "All keys already loaded" def FindUnloadedKeys(self, keys): + """Determines which keys have not been loaded yet. + + Args: + keys: dict as returned by FindKeys. + + Returns: + iterable of strings, paths to private key files to load. + """ loaded = set(self.GetLoadedKeys()) toload = set() for key, config in keys.iteritems(): @@ -249,10 +305,16 @@ class AgentManager(object): return toload def LoadKeyFiles(self, keys): + """Load all specified keys. + + Args: + keys: iterable of strings, each string a path to a key to load. + """ keys = " ".join(keys) self.RunShellCommandInAgent(self.agent_file, "ssh-add %s" % keys) def GetLoadedKeys(self): + """Returns an iterable of strings, each the fingerprint of a loaded key.""" retval, stdout = self.RunShellCommandInAgent(self.agent_file, "ssh-add -l") if retval != 0: return [] @@ -268,6 +330,7 @@ class AgentManager(object): @staticmethod def GetPublicKeyFingerprint(key): + """Returns the fingerprint of a public key as a string.""" retval, stdout = AgentManager.RunShellCommand("ssh-keygen -l -f %s |tr -s ' '" % key) if retval: return None @@ -280,6 +343,15 @@ class AgentManager(object): @staticmethod def GetAgentFile(path, identity): + """Returns the path to an agent config file. + + Args: + path: string, the path where agent config files are kept. + identity: string, identity for which to load the agent. + + Returns: + string, path to the agent file. + """ # Use the hostname as part of the path just in case this is on NFS. agentfile = os.path.join(path, "agent-%s-%s" % (identity, socket.gethostname())) if os.access(agentfile, os.R_OK) and AgentManager.IsAgentFileValid(agentfile): @@ -293,6 +365,7 @@ class AgentManager(object): @staticmethod def IsAgentFileValid(agentfile): + """Returns true if the specified agentfile refers to a running agent.""" retval, output = AgentManager.RunShellCommandInAgent(agentfile, "ssh-add -l >/dev/null 2>/dev/null") if retval & 0xff not in [0, 1]: print >>sys.stderr, "Agent in %s not running" % agentfile @@ -301,6 +374,7 @@ class AgentManager(object): @staticmethod def RunShellCommand(command): + """Runs a shell command, returns (status, stdout), (int, string).""" command = ["/bin/sh", "-c", command] process = subprocess.Popen(command, stdout=subprocess.PIPE) stdout, stderr = process.communicate() @@ -308,19 +382,23 @@ class AgentManager(object): @staticmethod def RunShellCommandInAgent(agentfile, command): - command = ["/usr/bin/env", "-i", "/bin/sh", "-c", ". %s >/dev/null 2>/dev/null; %s" % (agentfile, command)] + """Runs a shell command with an agent configured in the environment.""" + command = ["/usr/bin/env", "-i", "/bin/sh", "-c", + ". %s >/dev/null 2>/dev/null; %s" % (agentfile, command)] process = subprocess.Popen(command, stdout=subprocess.PIPE) stdout, stderr = process.communicate() return process.wait(), stdout @staticmethod def EscapeShellArguments(argv): + """Escapes all arguments to the shell, returns a string.""" escaped = [] for arg in argv: escaped.append("'%s'" % arg.replace("'", "'\"'\"'")) return " ".join(escaped) def RunSSH(self, argv): + """Execs ssh with the specified arguments.""" command = ["/bin/sh", "-c", ". %s >/dev/null 2>/dev/null; exec %s %s" % ( self.agent_file, self.config.Get("BINARY_SSH"), self.EscapeShellArguments(argv))]