Revision: 146
Author:   janne.t.harkonen
Date:     Wed Aug 22 21:55:51 2012
Log:      Expose @ssh_client, add config classes

@ssh_client exposes the underlying paramiko or trilead wrapper, this is to
enable easier programmatic use of SSHLibrary.

Also created separate classes to keep track of default and pre client
configurations.

Some random cleanup and documentation fixes are also included.
http://code.google.com/p/robotframework-sshlibrary/source/detail?r=146

Added:
 /trunk/utest/test_library.py
Modified:
 /trunk/atest/importing_with_args.txt
 /trunk/atest/interactive_session.txt
 /trunk/src/SSHLibrary/__init__.py
 /trunk/src/SSHLibrary/core.py
 /trunk/src/SSHLibrary/javaclient.py
 /trunk/src/SSHLibrary/pythonclient.py

=======================================
--- /dev/null
+++ /trunk/utest/test_library.py        Wed Aug 22 21:55:51 2012
@@ -0,0 +1,65 @@
+import unittest
+
+from SSHLibrary import SSHLibrary
+
+
+HOSTNAME = 'localhost'
+
+
+class TestSSHLibraryConfiguration(unittest.TestCase):
+
+    def test_default_confguration(self):
+        self._assert_config(SSHLibrary()._config)
+
+    def test_setting_configruation_values(self):
+        cfg = SSHLibrary(newline='CRLF', prompt='$')._config
+        self._assert_config(cfg, newline='\r\n', prompt='$')
+
+    def test_set_default_confguarition(self):
+        timeout, newline, prompt, level = 1, '\r\n', '>', 'DEBUG'
+        lib = SSHLibrary()
+        lib.set_default_configuration(timeout=timeout, newline=newline,
+                                      prompt=prompt, log_level=level)
+        self._assert_config(lib._config, timeout, newline, prompt, level)
+
+    def _assert_config(self, cfg, timeout=3, newline='\n', prompt=None,
+                       log_level='INFO'):
+        self.assertEquals(cfg.timeout, timeout)
+        self.assertEquals(cfg.newline, newline)
+        self.assertEquals(cfg.prompt, prompt)
+        self.assertEquals(cfg.log_level, log_level)
+
+
+class TestSSHClientConfiguration(unittest.TestCase):
+
+    def test_default_client_configuration(self):
+        lib = SSHLibrary()
+        lib.open_connection(HOSTNAME)
+        self._assert_config(lib.ssh_client.config)
+
+    def test_overriding_client_configuration(self):
+        lib = SSHLibrary(timeout=4)
+        lib.open_connection(HOSTNAME, timeout=5)
+        self._assert_config(lib.ssh_client.config, timeout=5)
+
+    def test_set_client_confguration(self):
+        port, term_type = 23, 'ansi'
+        lib = SSHLibrary()
+        lib.open_connection(HOSTNAME)
+        lib.set_client_configuration(port=port, term_type=term_type)
+        self._assert_config(lib.ssh_client.config, port=port,
+                            term_type=term_type)
+
+
+    def _assert_config(self, cfg, host=HOSTNAME, timeout=3, newline='\n',
+                       prompt=None, port=22, term_type='vt100'):
+        self.assertEquals(cfg.host, host)
+        self.assertEquals(cfg.timeout, timeout)
+        self.assertEquals(cfg.newline, newline)
+        self.assertEquals(cfg.prompt, prompt)
+        self.assertEquals(cfg.term_type, term_type)
+        self.assertEquals(cfg.port, port)
+
+
+if __name__ == '__main__':
+    unittest.main()
=======================================
--- /trunk/atest/importing_with_args.txt        Fri Aug 10 04:45:28 2012
+++ /trunk/atest/importing_with_args.txt        Wed Aug 22 21:55:51 2012
@@ -5,5 +5,6 @@
 Test Library Args
     ${default timeout} =  Set Timeout  1 minute
     Should Be Equal  ${default timeout}  3 minutes 30 seconds
-    ${default prompt} =  Set Default Prompt  >>
+    Open Connection    localhost
+    ${default prompt} =  Set Prompt  >>
     Should Be Equal  ${default prompt}  $
=======================================
--- /trunk/atest/interactive_session.txt        Fri Aug 10 04:45:28 2012
+++ /trunk/atest/interactive_session.txt        Wed Aug 22 21:55:51 2012
@@ -60,7 +60,9 @@
     Should Contain  ${output2}  ${PROMPT}

 Prompt Is Not Set
+    [Teardown]    Switch Connection    1
     Set Prompt  ${None}
+    Login as Valid User
     Run Keyword And Expect Error
... Using 'Read Until Prompt', 'Write' or 'Write Bare' keyword requires setting prompt first. *
     ...    Write    This should fail
=======================================
--- /trunk/src/SSHLibrary/__init__.py   Wed Aug 22 21:55:38 2012
+++ /trunk/src/SSHLibrary/__init__.py   Wed Aug 22 21:55:51 2012
@@ -21,7 +21,9 @@
 from robot import utils

 from connectioncache import ConnectionCache
-from client import AuthenticationException
+from core import AuthenticationException
+from config import (Configuration, StringEntry, NewlineEntry, TimeEntry,
+        IntegerEntry, LogLevelEntry)
 if utils.is_jython:
     from javaclient import JavaSSHClient as SSHClient
 else:
@@ -31,7 +33,31 @@
 __version__ = VERSION


+class DefaultConfig(Configuration):

+    def __init__(self, timeout, newline, prompt, log_level):
+        Configuration.__init__(self,
+                timeout=TimeEntry(timeout or 3),
+                newline=NewlineEntry(newline or 'LF'),
+                prompt=StringEntry(prompt),
+                log_level=LogLevelEntry(log_level or 'INFO'))
+
+
+class ClientConfig(Configuration):
+
+    def __init__(self, host, alias, port, timeout, newline, prompt,
+                 term_type, width, height, defaults):
+        Configuration.__init__(self,
+                host=StringEntry(host),
+                alias=StringEntry(alias),
+                port=IntegerEntry(port or 22),
+                timeout=TimeEntry(timeout or defaults.timeout),
+                newline=StringEntry(newline or defaults.newline),
+                prompt=StringEntry(prompt or defaults.prompt),
+                term_type=StringEntry(term_type or 'vt100'),
+                width=IntegerEntry(width or 80),
+                height=IntegerEntry(height or 24))
+

 class DeprecatedKeywords(object):
     """Mixin class containing deprecated keywords"""
@@ -93,24 +119,56 @@
     ROBOT_LIBRARY_SCOPE = 'GLOBAL'
     ROBOT_LIBRARY_VERSION = __version__

-    def __init__(self, timeout=3, newline='LF', prompt=None):
+    def __init__(self, timeout=3, newline='LF', prompt=None,
+                 log_level='INFO'):
         """SSH Library allows some import time configuration.

         `timeout`, `newline` and `prompt` set default values for new
-        connections opened with `Open Connection`.These values may be later
-        changed with `Set Timeout`, `Set Newline` and `Set Default Prompt`,
-        respectively.
+        connections opened with `Open Connection`. The default values may
+        later be changed using `Set Default Configuration` and settings
+        of a single connection with `Set Client Configuration`.
+
+        `log_level` sets the default log level used to log return values of
+        `Read Until` variants. It can also be later changed using `Set
+        Default Configuration`.

         Examples:
-        | Library  | SSHLibrary | # normal import |
- | Library | SSHLibrary | 10 | | > | # Set timeout and prompt, use default newline |
+        | Library | SSHLibrary | # use default values |
+        | Library | SSHLibrary | timeout=10 | prompt=> |
         """
         self._cache = ConnectionCache()
-        self._client = self._cache.current
-        self._newline = self._parse_newline(newline or 'LF')
-        self.set_timeout(timeout or 3)
-        self._default_log_level = 'INFO'
-        self._default_prompt = prompt
+        self._config = DefaultConfig(timeout, newline, prompt, log_level)
+
+    @property
+    def ssh_client(self):
+        return self._cache.current
+
+    def set_default_configuration(self, **entries):
+        """Update the default configuration values.
+
+ This keyword can only be used using named argument syntax. The names
+        of accepted arguments are the same as can be given in the `library
+        importing`.
+
+        Example:
+            | Set Default Configuration | newline=CRLF | prompt=$ |
+        """
+        self._config.update(**entries)
+
+    def set_client_configuration(self, **entries):
+        """Update the client configuration values.
+
+        Works on the currently selected connection. At least one connection
+        must have been opened using `Open Connection`.
+
+ This keyword can only be used using named argument syntax. The names
+        of accepted arguments are the same as can be given to the `Open
+        Connection` keyword.
+
+        Example:
+            | Set Client Configuration | term_type=ansi | timeout=2 hours |
+        """
+        self.ssh_client.config.update(**entries)

     def open_connection(self, host, alias=None, port=22, timeout=None,
                         newline=None, prompt=None, term_type='vt100',
@@ -128,28 +186,26 @@
         Connection` for more details about that.

If `timeout`, `newline` or `prompt` are not given, the default values
-        set in `library importing` are used. See  also `Set Timeout`,
-        `Set Newline` and `Set Prompt` for more information.
+        set in `library importing` are used. See also `Set Default
+        Configuration`.

-        Starting from SSHLibrary 1.1, a shell is also opened automatically.
-        `term_type` defines the terminal type for this shell, and `width`
-        and `height` can be configured to control the virtual size of it.
+        Starting from SSHLibrary 1.1, a shell session is also opened
+ automatically by this keyword. `term_type` defines the terminal type + for this shell, and `width` and `height` can be configured to control
+        the virtual size of it.
+
+        All of the client configuration options can be later updated using
+        `Set Client Configuration`.

         Examples:
-        | Open Connection | myhost.net      | | | | |
-        | Open Connection | yourhost.com    | 2nd conn   | # alias | |
- | ${id} = | Open Connection | myhost.net | # index to variable | | - | Open Connection | myhost.net | port=23 | # different port | | - | Open Connection | myhost.net | newline=CRLF | prompt=# | #set newline and prompt | - | Open Connection | myhost.net | term_type=ansi | width=40 | #confgiure shell |
+        | Open Connection | myhost.net |
+ | Open Connection | yourhost.com | alias=2nd conn | port=23 | prompt=# |
+        | Open Connection | myhost.net | term_type=ansi | width=40 |
+        | ${id} =         | Open Connection | myhost.net |
         """
-        self._host, self._port = host, port
-        self._timeout = int(timeout) if timeout else self._timeout
- self._newline = self._parse_newline(newline) if newline else self._newline
-        prompt = prompt and prompt or self._default_prompt
- self._term_type, self._width, self._height = term_type, width, height
-        self._client = SSHClient(host, int(port), prompt)
-        return self._cache.register(self._client, alias)
+        config = ClientConfig(host, alias, port, timeout, newline, prompt,
+                              term_type, width, height, self._config)
+        return self._cache.register(SSHClient(config), config.alias)

     def switch_connection(self, index_or_alias):
         """Switches between active connections using index or alias.
@@ -157,7 +213,7 @@
         Index is got from `Open Connection` and alias can be given to it.

Returns the index of previous connection, which can be used to restore - the connection later. This works with Robot Framework 2.0.2 or newer.
+        the connection later.

         Example:

@@ -183,7 +239,7 @@
         | Switch Connection  | ${id}           |            |
         """
         old_index = self._cache.current_index
-        self._client = self._cache.switch(index_or_alias)
+        self._cache.switch(index_or_alias)
         return old_index

     def close_all_connections(self):
@@ -198,7 +254,6 @@
             self._cache.close_all()
         except AttributeError:
             pass
-        self._client = self._cache.current

     def enable_ssh_logging(self, logfile):
         """Enables logging of SSH protocol output to given `logfile`
@@ -221,7 +276,7 @@
     def close_connection(self):
         """Closes the currently active connection.
         """
-        self._client.close()
+        self.ssh_client.close()

     def login(self, username, password):
         """Logs in to SSH server with given user information.
@@ -232,11 +287,11 @@
         Example:
         | Login | john | secret |
         """
-        self._info("Logging into '%s:%s' as '%s'."
-                    % (self._host, self._port, username))
-        self._client.login(username, password)
-        self._client.open_shell(self._term_type, self._width, self._height)
- return self.read_until_prompt() if self._client.prompt else self.read()
+        self._log_login(username)
+        self.ssh_client.login(username, password)
+        self.ssh_client.open_shell()
+ return self.read_until_prompt() if self.ssh_client.config.prompt else \
+                    self.read()

     def login_with_public_key(self, username, keyfile, password):
         """Logs into SSH server with using key-based authentication.
@@ -250,13 +305,17 @@

         """
         self._verify_key_file(keyfile)
-        self._info("Logging into '%s:%s' as '%s'."
-                    % (self._host, self._port, username))
+        self._log_login(username)
         try:
-            self._client.login_with_public_key(username, keyfile, password)
+ self.ssh_client.login_with_public_key(username, keyfile, password)
         except AuthenticationException:
             raise RuntimeError('Login with public key failed')
- return self.read_until_prompt() if self._client.prompt else self.read() + return self.read_until_prompt() if self.ssh_client.config.prompt else \
+                    self.read()
+
+    def _log_login(self, username):
+        self._info("Logging into '%s:%s' as '%s'."
+ % (self.ssh_client.host, self.ssh_client.port, username))

     def _verify_key_file(self, keyfile):
         if not os.path.exists(keyfile):
@@ -297,7 +356,7 @@
         """
         self._info("Executing command '%s'" % command)
opts = self._output_options(return_stdout, return_stderr, return_rc)
-        return self._client.execute_command(command, *opts)
+        return self.ssh_client.execute_command(command, *opts)

     def start_command(self, command):
         """Starts command execution on remote host.
@@ -313,7 +372,7 @@
         """
         self._info("Starting command '%s'" % command)
         self._command = command
-        self._client.start_command(command)
+        self.ssh_client.start_command(command)

     def read_command_output(self, return_stdout=True, return_stderr=False,
                             return_rc=False):
@@ -327,7 +386,7 @@
         """
         self._info("Reading output of command '%s'" % self._command)
opts = self._output_options(return_stdout, return_stderr, return_rc)
-        return self._client.read_command_output(*opts)
+        return self.ssh_client.read_command_output(*opts)

     def _output_options(self, stdout, stderr, rc):
         # Handle legacy options for configuring returned outputs
@@ -350,8 +409,8 @@
         command. To get the output, one of the `Read XXX` keywords must be
         used.
         """
-        self.write_bare(text + self._newline)
-        return self.read_until(self._newline, loglevel)
+        self.write_bare(text + self._config.newline)
+        return self.read_until(self._config.newline, loglevel)

     def write_bare(self, text):
         """Writes given text over the connection without appending newline.
@@ -366,10 +425,10 @@
         self._ensure_prompt_is_set()
         self._ensure_open_shell(for_writing=True)
         self._info("Writing %s" % repr(text))
-        self._client.write(text)
+        self.ssh_client.write(text)

     def _ensure_open_shell(self, for_writing=False):
-        if self._client.shell is None:
+        if self.ssh_client.shell is None:
             self.open_shell()
             if for_writing:
                 self.read_until_prompt('INFO')
@@ -386,7 +445,7 @@
         buffer, thus clearing it.
         """
         self._ensure_open_shell()
-        ret = self._client.read()
+        ret = self.ssh_client.read()
         self._log(ret, loglevel)
         return ret

@@ -407,8 +466,8 @@
         self._ensure_open_shell()
         ret = ''
         start_time = time.time()
-        while time.time() < float(self._timeout) + start_time:
-            ret += self._client.read_char()
+        while time.time() < float(self._config.timeout) + start_time:
+            ret += self.ssh_client.read_char()
             if (isinstance(expected, basestring) and expected in ret) or \
(not isinstance(expected, basestring) and expected.search(ret)):
                 self._log(ret, loglevel)
@@ -417,7 +476,7 @@
         if not isinstance(expected, basestring):
             expected = expected.pattern
         raise AssertionError("No match found for '%s' in %s"
-                % (expected, utils.secs_to_timestr(self._timeout)))
+                % (expected, utils.secs_to_timestr(self._config.timeout)))

     def read_until_regexp(self, regexp, loglevel=None):
"""Reads output until a match to `regexp` is found or timeout expires.
@@ -451,10 +510,10 @@
         produce prompt characters in its output.
         """
         self._ensure_prompt_is_set()
-        return self.read_until(self._client.prompt, loglevel)
+        return self.read_until(self.ssh_client.config.prompt, loglevel)

     def _ensure_prompt_is_set(self):
-        if self._client.prompt is None:
+        if not self.ssh_client.config.prompt:
             raise RuntimeError("Using 'Read Until Prompt', 'Write' or "
                 "'Write Bare' keyword requires setting prompt first. "
                 "Prompt can be set either when taking library into use or "
@@ -534,16 +593,16 @@

         """
         mode = int(mode, 8)
-        self._client.create_sftp_client()
+        self.ssh_client.create_sftp_client()
         localfiles = self._get_put_file_sources(source)
remotefiles, remotepath = self._get_put_file_destinations(localfiles, destination)
-        self._client.create_missing_remote_path(remotepath)
+        self.ssh_client.create_missing_remote_path(remotepath)
         for src, dst in zip(localfiles, remotefiles):
             self._info("Putting '%s' to '%s'" % (src, dst))
             newline = {'CRLF': '\r\n', 'LF': '\n'}.get(newlines, None)
-            self._client.put_file(src, dst, mode, newline)
-        self._client.close_sftp_client()
+            self.ssh_client.put_file(src, dst, mode, newline)
+        self.ssh_client.close_sftp_client()

     def _get_put_file_sources(self, source):
         sources = [f for f in glob.glob(source.replace('/', os.sep))
@@ -558,7 +617,7 @@
     def _get_put_file_destinations(self, sources, dest):
         dest = dest.split(':')[-1].replace('\\', '/')
         if dest == '.':
-            dest = self._client.homedir + '/'
+            dest = self.ssh_client.homedir + '/'
         if len(sources) > 1 and dest[-1] != '/':
             raise ValueError('It is not possible to copy multiple source '
                              'files to one destination file.')
@@ -572,7 +631,7 @@

     def _parse_path_elements(self, dest):
         if not posixpath.isabs(dest):
-            dest = posixpath.join(self._client.homedir, dest)
+            dest = posixpath.join(self.ssh_client.homedir, dest)
         return posixpath.split(dest)

     def get_file(self, source, destination='.'):
@@ -604,22 +663,22 @@
| Get File | /path_to_remote_files/*.txt | /path_to_local_files/ | # multiple files with wild cards |

         """
-        self._client.create_sftp_client()
+        self.ssh_client.create_sftp_client()
         remotefiles = self._get_get_file_sources(source)
         self._debug('Source pattern matched remote files: %s' %
                     utils.seq2str(remotefiles))
localfiles = self._get_get_file_destinations(remotefiles, destination)
         for src, dst in zip(remotefiles, localfiles):
             self._info('Getting %s to %s' % (src, dst))
-            self._client.get_file(src, dst)
-        self._client.close_sftp_client()
+            self.ssh_client.get_file(src, dst)
+        self.ssh_client.close_sftp_client()

     def _get_get_file_sources(self, source):
         path, pattern = posixpath.split(source)
         if not path:
             path = '.'
         sourcefiles = []
-        for filename in self._client.listfiles(path):
+        for filename in self.ssh_client.listfiles(path):
             if utils.matches(filename, pattern):
                 if path:
                     filename = posixpath.join(path, filename)
@@ -661,7 +720,7 @@
         self._is_valid_log_level(level, raise_if_invalid=True)
         msg = msg.strip()
         if level is None:
-            level = self._default_log_level
+            level = self._config.log_level
         if msg != '':
             print '*%s* %s' % (level.upper(), msg)

=======================================
--- /trunk/src/SSHLibrary/core.py       Thu Aug 16 04:17:12 2012
+++ /trunk/src/SSHLibrary/core.py       Wed Aug 22 21:55:51 2012
@@ -42,10 +42,10 @@

 class SSHClient(object):

-    def __init__(self, host, port, prompt):
-        self.host = host
-        self.port = port
-        self.prompt = prompt
+    def __init__(self, config):
+        self.host = config.host
+        self.port = config.port
+        self.config = config
         self.shell = None
         self.client = self._create_client()
         self._commands = []
=======================================
--- /trunk/src/SSHLibrary/javaclient.py Thu Aug 16 04:17:12 2012
+++ /trunk/src/SSHLibrary/javaclient.py Wed Aug 22 21:55:51 2012
@@ -55,9 +55,10 @@
         cmd.run_in(self.client.openSession())
         return cmd

-    def open_shell(self, term_type, width, height):
+    def open_shell(self):
         self.shell = self.client.openSession()
-        self.shell.requestPTY(term_type, width, height, 0, 0, None)
+        self.shell.requestPTY(self.config.term_type, self.config.width,
+                self.config.height, 0, 0, None)
         self.shell.startShell()
         self._writer = self.shell.getStdin()
         self._stdout = self.shell.getStdout()
=======================================
--- /trunk/src/SSHLibrary/pythonclient.py       Thu Aug 16 04:17:12 2012
+++ /trunk/src/SSHLibrary/pythonclient.py       Wed Aug 22 21:55:51 2012
@@ -63,8 +63,9 @@
         cmd.run_in(self.client.get_transport().open_session())
         return cmd

-    def open_shell(self, term_type, width, height):
-        self.shell = self.client.invoke_shell(term_type, width, height)
+    def open_shell(self):
+        self.shell = self.client.invoke_shell(self.config.term_type,
+                self.config.width, self.config.height)

     def write(self, text):
         self.shell.sendall(text)

Reply via email to