
This change adds the capability to satori to do data plane discovery on Windows devices. For that, a class has been added that mirrors the functionality of satori/ssh.py and utilizes a "3rd party" script (satori/contrib/psexec.py) which is called via subprocess.Popen(). Further to that, an SSH tunneling class has been put in place that uses paramiko to establish a tunnel (similar to running ssh -L from a shell). requirements.txt has been extended to include impacket which satori/contrib/psexec.py imports. Move support for PoSH-Ohai into its own provider module. Raise UnsupportedPlatform exceptions in ohai_solo.py when the client is non-linux, and raise the same exception in posh_ohai.py when the client is non-windows. Co-Authored-By: Nico Engelen <engelen.nico@googlemail.com> Co-Authored-By: Samuel Stavinoha <samuel.stavinoha@rackspace.com> Change-Id: I7a94eea9446bc7f57843407fb98880222f7af6af Implements: blueprint windows-support
168 lines
5.2 KiB
Python
168 lines
5.2 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
|
|
"""SSH tunneling module.
|
|
|
|
Set up a forward tunnel across an SSH server, using paramiko. A local port
|
|
(given with -p) is forwarded across an SSH session to an address:port from
|
|
the SSH server. This is similar to the openssh -L option.
|
|
"""
|
|
try:
|
|
import eventlet
|
|
eventlet.monkey_patch()
|
|
from eventlet.green import threading
|
|
from eventlet.green import time
|
|
except ImportError:
|
|
import threading
|
|
import time
|
|
pass
|
|
|
|
import logging
|
|
import select
|
|
import socket
|
|
try:
|
|
import SocketServer
|
|
except ImportError:
|
|
import socketserver as SocketServer
|
|
|
|
import paramiko
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class TunnelServer(SocketServer.ThreadingTCPServer):
|
|
|
|
"""Serve on a local ephemeral port.
|
|
|
|
Clients will connect to that port/server.
|
|
"""
|
|
|
|
daemon_threads = True
|
|
allow_reuse_address = True
|
|
|
|
|
|
class TunnelHandler(SocketServer.BaseRequestHandler):
|
|
|
|
"""Handle forwarding of packets."""
|
|
|
|
def handle(self):
|
|
"""Do all the work required to service a request.
|
|
|
|
The request is available as self.request, the client address as
|
|
self.client_address, and the server instance as self.server, in
|
|
case it needs to access per-server information.
|
|
|
|
This implementation will forward packets.
|
|
"""
|
|
try:
|
|
chan = self.ssh_transport.open_channel('direct-tcpip',
|
|
self.target_address,
|
|
self.request.getpeername())
|
|
except Exception as exc:
|
|
LOG.error('Incoming request to %s:%s failed',
|
|
self.target_address[0],
|
|
self.target_address[1],
|
|
exc_info=exc)
|
|
return
|
|
if chan is None:
|
|
LOG.error('Incoming request to %s:%s was rejected '
|
|
'by the SSH server.',
|
|
self.target_address[0],
|
|
self.target_address[1])
|
|
return
|
|
|
|
while True:
|
|
r, w, x = select.select([self.request, chan], [], [])
|
|
if self.request in r:
|
|
data = self.request.recv(1024)
|
|
if len(data) == 0:
|
|
break
|
|
chan.send(data)
|
|
if chan in r:
|
|
data = chan.recv(1024)
|
|
if len(data) == 0:
|
|
break
|
|
self.request.send(data)
|
|
|
|
try:
|
|
peername = None
|
|
peername = str(self.request.getpeername())
|
|
except socket.error as exc:
|
|
LOG.warning("Couldn't fetch peername.", exc_info=exc)
|
|
chan.close()
|
|
self.request.close()
|
|
LOG.info("Tunnel closed from '%s'", peername or 'unnamed peer')
|
|
|
|
|
|
class Tunnel(object): # pylint: disable=R0902
|
|
|
|
"""Create a TCP server which will use TunnelHandler."""
|
|
|
|
def __init__(self, target_host, target_port,
|
|
sshclient, tunnel_host='localhost',
|
|
tunnel_port=0):
|
|
"""Constructor."""
|
|
if not isinstance(sshclient, paramiko.SSHClient):
|
|
raise TypeError("'sshclient' must be an instance of "
|
|
"paramiko.SSHClient.")
|
|
|
|
self.target_host = target_host
|
|
self.target_port = target_port
|
|
self.target_address = (target_host, target_port)
|
|
self.address = (tunnel_host, tunnel_port)
|
|
|
|
self._tunnel = None
|
|
self._tunnel_thread = None
|
|
self.sshclient = sshclient
|
|
self._ssh_transport = self.get_sshclient_transport(
|
|
self.sshclient)
|
|
|
|
TunnelHandler.target_address = self.target_address
|
|
TunnelHandler.ssh_transport = self._ssh_transport
|
|
|
|
self._tunnel = TunnelServer(self.address, TunnelHandler)
|
|
# reset attribute to the port it has actually been set to
|
|
self.address = self._tunnel.server_address
|
|
tunnel_host, self.tunnel_port = self.address
|
|
|
|
def get_sshclient_transport(self, sshclient):
|
|
"""Get the sshclient's transport.
|
|
|
|
Connect the sshclient, that has been passed in and return its
|
|
transport.
|
|
"""
|
|
sshclient.connect()
|
|
return sshclient.get_transport()
|
|
|
|
def serve_forever(self, async=True):
|
|
"""Serve the tunnel forever.
|
|
|
|
if async is True, this will be done in a background thread
|
|
"""
|
|
if not async:
|
|
self._tunnel.serve_forever()
|
|
else:
|
|
self._tunnel_thread = threading.Thread(
|
|
target=self._tunnel.serve_forever)
|
|
self.start()
|
|
# cooperative yield
|
|
time.sleep(0)
|
|
|
|
def shutdown(self):
|
|
"""Stop serving the tunnel.
|
|
|
|
Also close the socket.
|
|
"""
|
|
self._tunnel.shutdown()
|
|
self._tunnel.socket.close()
|