Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-parallax for openSUSE:Factory checked in at 2022-11-07 13:51:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-parallax (Old) and /work/SRC/openSUSE:Factory/.python-parallax.new.1597 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-parallax" Mon Nov 7 13:51:26 2022 rev:20 rq:1033892 version:1.0.8 Changes: -------- --- /work/SRC/openSUSE:Factory/python-parallax/python-parallax.changes 2022-09-30 17:58:31.773342896 +0200 +++ /work/SRC/openSUSE:Factory/.python-parallax.new.1597/python-parallax.changes 2022-11-07 13:51:29.779735736 +0100 @@ -1,0 +2,17 @@ +Mon Nov 7 02:18:35 UTC 2022 - XinLiang <xli...@suse.com> + +- Fix: manager: file descriptor leakage +- Release 1.0.8 + +------------------------------------------------------------------- +Thu Nov 3 02:16:52 UTC 2022 - XinLiang <xli...@suse.com> + +- Release 1.0.7 +- Remove patches since already included: + Remove patch 0001-Add-ssh_key-option-used-by-i-option-of-ssh-scp.patch + Remove patch 0002-Change-format-of-scp-command-for-ipv6-compatible.patch + Remove patch 0003-Fix-task-Don-t-use-ssh-if-command-running-on-local-b.patch + Remove patch 0004-Fix-Error-inherit-from-Exception-instead-of-BaseExce.patch + Remove patch 0005-Dev-add-parallax.run-to-return-non-zero-rc-without-r.patch + +------------------------------------------------------------------- Old: ---- 0001-Add-ssh_key-option-used-by-i-option-of-ssh-scp.patch 0002-Change-format-of-scp-command-for-ipv6-compatible.patch 0003-Fix-task-Don-t-use-ssh-if-command-running-on-local-b.patch 0004-Fix-Error-inherit-from-Exception-instead-of-BaseExce.patch 0005-Dev-add-parallax.run-to-return-non-zero-rc-without-r.patch parallax-1.0.6.tar.gz New: ---- parallax-1.0.8.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-parallax.spec ++++++ --- /var/tmp/diff_new_pack.CR9Clt/_old 2022-11-07 13:51:30.291738615 +0100 +++ /var/tmp/diff_new_pack.CR9Clt/_new 2022-11-07 13:51:30.295738637 +0100 @@ -18,18 +18,13 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-parallax -Version: 1.0.6 +Version: 1.0.8 Release: 0 Summary: Python module for multi-node SSH command execution and file copy License: BSD-3-Clause Group: Development/Languages/Python URL: https://github.com/krig/parallax/ Source: https://files.pythonhosted.org/packages/source/p/parallax/parallax-%{version}.tar.gz -Patch1: 0001-Add-ssh_key-option-used-by-i-option-of-ssh-scp.patch -Patch2: 0002-Change-format-of-scp-command-for-ipv6-compatible.patch -Patch3: 0003-Fix-task-Don-t-use-ssh-if-command-running-on-local-b.patch -Patch4: 0004-Fix-Error-inherit-from-Exception-instead-of-BaseExce.patch -Patch5: 0005-Dev-add-parallax.run-to-return-non-zero-rc-without-r.patch BuildRequires: %{python_module setuptools} BuildRequires: fdupes @@ -54,11 +49,6 @@ %prep %setup -q -n parallax-%{version} -%patch1 -p1 -%patch2 -p1 -%patch3 -p1 -%patch4 -p1 -%patch5 -p1 %build %python_build ++++++ parallax-1.0.6.tar.gz -> parallax-1.0.8.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/.gitignore new/parallax-1.0.8/.gitignore --- old/parallax-1.0.6/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/parallax-1.0.8/.gitignore 2022-11-07 03:08:20.000000000 +0100 @@ -0,0 +1 @@ +*.pyc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/.travis.yml new/parallax-1.0.8/.travis.yml --- old/parallax-1.0.6/.travis.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/parallax-1.0.8/.travis.yml 2022-11-07 03:08:20.000000000 +0100 @@ -0,0 +1,8 @@ +--- +language: python +python: + - "2.6" + - "2.7" + - "3.4" + - "3.5" +script: nosetests --with-coverage diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/PKG-INFO new/parallax-1.0.8/PKG-INFO --- old/parallax-1.0.6/PKG-INFO 2020-04-01 10:50:03.678191400 +0200 +++ new/parallax-1.0.8/PKG-INFO 2022-11-07 03:08:20.000000000 +0100 @@ -1,31 +1,10 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.0 Name: parallax -Version: 1.0.6 -Summary: Execute commands and copy files over SSH to multiple machines at once +Version: 1.0.8 +Summary: UNKNOWN Home-page: https://github.com/krig/parallax/ Author: Kristoffer Gronlund -Author-email: k...@koru.se +Author-email: UNKNOWN License: BSD -Description: Parallax SSH provides an interface to executing commands on multiple - nodes at once using SSH. It also provides commands for sending and receiving files to - multiple nodes using SCP. -Platform: linux -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.1 -Classifier: Programming Language :: Python :: 3.2 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: System :: Clustering -Classifier: Topic :: System :: Networking -Classifier: Topic :: System :: Systems Administration +Description: UNKNOWN +Platform: UNKNOWN diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/README.md new/parallax-1.0.8/README.md --- old/parallax-1.0.6/README.md 2020-04-01 10:32:35.000000000 +0200 +++ new/parallax-1.0.8/README.md 2022-11-07 03:08:20.000000000 +0100 @@ -32,10 +32,17 @@ Executes the given command on a set of hosts, collecting the output. - Returns a dict mapping the hostname of - each host either to a tuple containing a return code, - stdout and stderr, or an `parallax.Error` instance - describing the error. + Returns a dict mapping the hostname of each host either to a tuple containing + a return code, stdout and stderr when return code is 0, or an `parallax.Error` + instance describing the error when return code is not 0. + +* `parallax.run(hosts, cmdline, opts)` + + Executes the given command on a set of hosts, collecting the output. + + Returns a dict mapping the hostname of each host either to a tuple containing + a return code, stdout and stderr, or an `parallax.Error` instance describing + the error when ssh error occurred. * `parallax.copy(hosts, src, dst, opts)` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax/__init__.py new/parallax-1.0.8/parallax/__init__.py --- old/parallax-1.0.6/parallax/__init__.py 2020-04-01 10:32:35.000000000 +0200 +++ new/parallax-1.0.8/parallax/__init__.py 2022-11-07 03:08:20.000000000 +0100 @@ -27,6 +27,7 @@ import os import sys +import socket DEFAULT_PARALLELISM = 32 DEFAULT_TIMEOUT = 0 # "infinity" by default @@ -54,14 +55,14 @@ return s -class Error(BaseException): +class Error(Exception): """ Returned instead of a result for a host in case of an error during the processing for that host. """ def __init__(self, msg, task): - super(BaseException, self).__init__() + super(Exception, self).__init__() self.msg = msg self.task = task @@ -87,6 +88,7 @@ askpass = False # Ask for a password outdir = None # Write stdout to a file per host in this directory errdir = None # Write stderr to a file per host in this directory + ssh_key = None # Specific ssh key used by ssh/scp -i option ssh_options = [] # Extra options to pass to SSH ssh_extra = [] # Extra arguments to pass to SSH verbose = False # Warning and diagnostic messages @@ -138,19 +140,21 @@ return ret -def _build_call_cmd(host, port, user, cmdline, options, extra): +def _build_call_cmd(host, port, user, cmdline, opts): cmd = ['ssh', host, '-o', 'NumberOfPasswordPrompts=1', '-o', 'SendEnv=PARALLAX_NODENUM PARALLAX_HOST'] - if options: - for opt in options: + if opts.ssh_options: + for opt in opts.ssh_options: cmd += ['-o', opt] if user: cmd += ['-l', user] if port: cmd += ['-p', port] - if extra: - cmd.extend(extra) + if opts.ssh_key: + cmd += ['-i', opts.ssh_key] + if opts.ssh_extra: + cmd.extend(opts.ssh_extra) if cmdline: cmd.append(cmdline) return cmd @@ -158,7 +162,7 @@ def call(hosts, cmdline, opts=Options()): """ - Executes the given command on a set of hosts, collecting the output + Executes the given command on a set of hosts, collecting the output. Return Error when exit status != 0. Returns {host: (rc, stdout, stdin) | Error} """ if opts.outdir and not os.path.exists(opts.outdir): @@ -173,9 +177,11 @@ warn_message=opts.warn_message, callbacks=_CallOutputBuilder()) for host, port, user in _expand_host_port_user(hosts): - cmd = _build_call_cmd(host, port, user, cmdline, - options=opts.ssh_options, - extra=opts.ssh_extra) + is_local = is_local_host(host) + if is_local: + cmd = [cmdline] + else: + cmd = _build_call_cmd(host, port, user, cmdline, opts) t = Task(host, port, user, cmd, stdin=opts.input_stream, verbose=opts.verbose, @@ -183,7 +189,8 @@ print_out=opts.print_out, inline=opts.inline, inline_stdout=opts.inline_stdout, - default_user=opts.default_user) + default_user=opts.default_user, + is_local=is_local) manager.add_task(t) try: return manager.run() @@ -219,13 +226,15 @@ cmd += ['-P', port] if opts.recursive: cmd.append('-r') + if opts.ssh_key: + cmd += ['-i', opts.ssh_key] if opts.ssh_extra: cmd.extend(opts.ssh_extra) cmd.append(src) if user: - cmd.append('%s@%s:%s' % (user, host, dst)) + cmd.append('%s@[%s]:%s' % (user, host, dst)) else: - cmd.append('%s:%s' % (host, dst)) + cmd.append('[%s]:%s' % (host, dst)) return cmd @@ -312,12 +321,14 @@ cmd += ['-P', port] if opts.recursive: cmd.append('-r') + if opts.ssh_key: + cmd += ['-i', opts.ssh_key] if opts.ssh_extra: cmd.extend(opts.ssh_extra) if user: - cmd.append('%s@%s:%s' % (user, host, src)) + cmd.append('%s@[%s]:%s' % (user, host, src)) else: - cmd.append('%s:%s' % (host, src)) + cmd.append('[%s]:%s' % (host, src)) cmd.append(dst) return cmd @@ -361,3 +372,73 @@ return manager.run() except FatalError as err: raise IOError(str(err)) + + +def is_local_host(host): + """ + Check if the host is local + """ + try: + socket.inet_aton(host) + hostname = socket.gethostbyaddr(host)[0] + except: + hostname = host + return hostname == socket.gethostname() + +def run(hosts, cmdline, opts=Options()): + """ + Executes the given command on a set of hosts, collecting the output. Return Error when ssh error occurred. + Returns {host: (rc, stdout, stdin) | Error} + """ + if opts.outdir and not os.path.exists(opts.outdir): + os.makedirs(opts.outdir) + if opts.errdir and not os.path.exists(opts.errdir): + os.makedirs(opts.errdir) + manager = Manager(limit=opts.limit, + timeout=opts.timeout, + askpass=opts.askpass, + outdir=opts.outdir, + errdir=opts.errdir, + warn_message=opts.warn_message, + callbacks=_RunOutputBuilder()) + for host, port, user in _expand_host_port_user(hosts): + is_local = is_local_host(host) + if is_local: + cmd = [cmdline] + else: + cmd = _build_call_cmd(host, port, user, cmdline, opts) + t = Task(host, port, user, cmd, + stdin=opts.input_stream, + verbose=opts.verbose, + quiet=opts.quiet, + print_out=opts.print_out, + inline=opts.inline, + inline_stdout=opts.inline_stdout, + default_user=opts.default_user, + is_local=is_local) + manager.add_task(t) + try: + return manager.run() + except FatalError as err: + raise IOError(str(err)) + + +class _RunOutputBuilder(object): + def __init__(self): + self.finished_tasks = [] + + def finished(self, task, n): + """Called when Task is complete""" + self.finished_tasks.append(task) + + def result(self, manager): + """Called when all Tasks are complete to generate result""" + ret = {} + for task in self.finished_tasks: + if task.exitstatus == 255: + ret[task.host] = Error(', '.join(task.failures), task) + else: + ret[task.host] = (task.exitstatus, + task.outputbuffer or manager.outdir, + task.errorbuffer or manager.errdir) + return ret diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax/manager.py new/parallax-1.0.8/parallax/manager.py --- old/parallax-1.0.6/parallax/manager.py 2020-04-01 10:32:35.000000000 +0200 +++ new/parallax-1.0.8/parallax/manager.py 2022-11-07 03:08:20.000000000 +0100 @@ -4,7 +4,6 @@ from errno import EINTR import os import select -import signal import sys import threading import copy @@ -92,20 +91,20 @@ def run(self): """Processes tasks previously added with add_task.""" self.save_tasks = copy.copy(self.tasks) + if self.outdir or self.errdir: + writer = Writer(self.outdir, self.errdir) + writer.start() + else: + writer = None + try: - if self.outdir or self.errdir: - writer = Writer(self.outdir, self.errdir) + if writer: writer.start() - else: - writer = None - if self.askpass: pass_server = PasswordServer() pass_server.start(self.iomap, self.limit, warn=self.warn_message) self.askpass_socket = pass_server.address - self.set_sigchld_handler() - try: self.update_tasks(writer) wait = None @@ -116,46 +115,17 @@ self.iomap.poll(wait) self.update_tasks(writer) wait = self.check_timeout() + return self.callbacks.result(self) except KeyboardInterrupt: # This exception handler tries to clean things up and prints # out a nice status message for each interrupted host. self.interrupted() + raise + finally: + if writer: + writer.signal_quit() + writer.join() - except KeyboardInterrupt: - # This exception handler doesn't print out any fancy status - # information--it just stops. - pass - - if writer: - writer.signal_quit() - writer.join() - - return self.callbacks.result(self) - - def clear_sigchld_handler(self): - signal.signal(signal.SIGCHLD, signal.SIG_DFL) - - def set_sigchld_handler(self): - # TODO: find out whether set_wakeup_fd still works if the default - # signal handler is used (I'm pretty sure it doesn't work if the - # signal is ignored). - signal.signal(signal.SIGCHLD, self.handle_sigchld) - # This should keep reads and writes from getting EINTR. - if hasattr(signal, 'siginterrupt'): - signal.siginterrupt(signal.SIGCHLD, False) - - def handle_sigchld(self, number, frame): - """Apparently we need a sigchld handler to make set_wakeup_fd work.""" - # Write to the signal pipe (only for Python <2.5, where the - # set_wakeup_fd method doesn't exist). - if self.iomap.wakeup_writefd: - os.write(self.iomap.wakeup_writefd, '\0') - for task in self.running: - if task.proc: - task.proc.poll() - # Apparently some UNIX systems automatically reset the SIGCHLD - # handler to SIG_DFL. Reset it just in case. - self.set_sigchld_handler() def add_task(self, task): """Adds a Task to be processed with run().""" @@ -236,17 +206,6 @@ self.readmap = {} self.writemap = {} - # Setup the wakeup file descriptor to avoid hanging on lost signals. - wakeup_readfd, wakeup_writefd = os.pipe() - fcntl.fcntl(wakeup_writefd, fcntl.F_SETFL, os.O_NONBLOCK) - self.register_read(wakeup_readfd, self.wakeup_handler) - # TODO: remove test when we stop supporting Python <2.5 - if hasattr(signal, 'set_wakeup_fd'): - signal.set_wakeup_fd(wakeup_writefd) - self.wakeup_writefd = None - else: - self.wakeup_writefd = wakeup_writefd - def register_read(self, fd, handler): """Registers an IO handler for a file descriptor for reading.""" self.readmap[fd] = handler @@ -284,21 +243,6 @@ handler = self.writemap[fd] handler(fd, self) - def wakeup_handler(self, fd, iomap): - """Handles read events on the signal wakeup pipe. - - This ensures that SIGCHLD signals aren't lost. - """ - try: - os.read(fd, READ_SIZE) - except (OSError, IOError): - _, e, _ = sys.exc_info() - errno, message = e.args - if errno != EINTR: - sys.stderr.write('Fatal error reading from wakeup pipe: %s\n' - % message) - raise FatalError - class PollIOMap(IOMap): """A manager for file descriptors and their associated handlers. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax/task.py new/parallax-1.0.8/parallax/task.py --- old/parallax-1.0.6/parallax/task.py 2020-04-01 10:32:35.000000000 +0200 +++ new/parallax-1.0.8/parallax/task.py 2022-11-07 03:08:20.000000000 +0100 @@ -39,7 +39,8 @@ print_out=False, inline=False, inline_stdout=False, - default_user=None): + default_user=None, + is_local=False): # Backwards compatibility: if not isinstance(verbose, bool): @@ -66,6 +67,7 @@ self.pretty_host = host self.port = port self.cmd = cmd + self.is_local = is_local if user and user != default_user: self.pretty_host = '@'.join((user, self.pretty_host)) @@ -126,7 +128,7 @@ close_fds=False, preexec_fn=os.setsid, env=environ) else: self.proc = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, - close_fds=False, start_new_session=True, env=environ) + close_fds=False, start_new_session=True, env=environ, shell=self.is_local) self.timestamp = time.time() if self.inputbuffer: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax/version.py new/parallax-1.0.8/parallax/version.py --- old/parallax-1.0.6/parallax/version.py 2020-04-01 10:33:22.000000000 +0200 +++ new/parallax-1.0.8/parallax/version.py 2022-11-07 03:08:20.000000000 +0100 @@ -1 +1 @@ -VERSION = '1.0.6' +VERSION = '1.0.8' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax.egg-info/PKG-INFO new/parallax-1.0.8/parallax.egg-info/PKG-INFO --- old/parallax-1.0.6/parallax.egg-info/PKG-INFO 2020-04-01 10:50:03.000000000 +0200 +++ new/parallax-1.0.8/parallax.egg-info/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,31 +0,0 @@ -Metadata-Version: 1.1 -Name: parallax -Version: 1.0.6 -Summary: Execute commands and copy files over SSH to multiple machines at once -Home-page: https://github.com/krig/parallax/ -Author: Kristoffer Gronlund -Author-email: k...@koru.se -License: BSD -Description: Parallax SSH provides an interface to executing commands on multiple - nodes at once using SSH. It also provides commands for sending and receiving files to - multiple nodes using SCP. -Platform: linux -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.1 -Classifier: Programming Language :: Python :: 3.2 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: System :: Clustering -Classifier: Topic :: System :: Networking -Classifier: Topic :: System :: Systems Administration diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax.egg-info/SOURCES.txt new/parallax-1.0.8/parallax.egg-info/SOURCES.txt --- old/parallax-1.0.6/parallax.egg-info/SOURCES.txt 2020-04-01 10:50:03.000000000 +0200 +++ new/parallax-1.0.8/parallax.egg-info/SOURCES.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1,20 +0,0 @@ -AUTHORS -COPYING -MANIFEST.in -README.md -setup.py -bin/parallax-askpass -parallax/__init__.py -parallax/askpass_client.py -parallax/askpass_server.py -parallax/callbacks.py -parallax/color.py -parallax/manager.py -parallax/psshutil.py -parallax/task.py -parallax/version.py -parallax.egg-info/PKG-INFO -parallax.egg-info/SOURCES.txt -parallax.egg-info/dependency_links.txt -parallax.egg-info/top_level.txt -test/test_api.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax.egg-info/dependency_links.txt new/parallax-1.0.8/parallax.egg-info/dependency_links.txt --- old/parallax-1.0.6/parallax.egg-info/dependency_links.txt 2020-04-01 10:50:03.000000000 +0200 +++ new/parallax-1.0.8/parallax.egg-info/dependency_links.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/parallax.egg-info/top_level.txt new/parallax-1.0.8/parallax.egg-info/top_level.txt --- old/parallax-1.0.6/parallax.egg-info/top_level.txt 2020-04-01 10:50:03.000000000 +0200 +++ new/parallax-1.0.8/parallax.egg-info/top_level.txt 1970-01-01 01:00:00.000000000 +0100 @@ -1 +0,0 @@ -parallax diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/parallax-1.0.6/setup.cfg new/parallax-1.0.8/setup.cfg --- old/parallax-1.0.6/setup.cfg 2020-04-01 10:50:03.678191400 +0200 +++ new/parallax-1.0.8/setup.cfg 1970-01-01 01:00:00.000000000 +0100 @@ -1,4 +0,0 @@ -[egg_info] -tag_build = -tag_date = 0 -