Psycopg2 access PostgreSQL database on remote host without manually opening ssh tunnel
Asked Answered
T

7

21

My standard procedure for accessing a PostgreSQL database on a remote server is to first create an ssh tunnel as:

ssh [email protected] -L 5432:localhost:5432 -p 222

and then run my query in python from another shell as:

conn = psycopg2.connect("host=localhost" + " dbname=" +
                         conf.dbname + " user=" + conf.user + 
                         " password=" + conf.password)

cur = conn.cursor()

cur.execute(query)

This piece of python code works nicely once the tunnel is created. However, I would like psycopg2 to already open the SSH tunnel or reach "somehow" the remote database without need to redirect it on my localhost.

Is it possible to do this with psycopg2?

Is otherwise possible open the ssh tunnel in my python code?

if I use:

os.system("ssh [email protected] -L 5432:localhost:5432 -p 222")

The shell will be redirected to the remote host blocking the execution of main thread.

Treadle answered 26/2, 2014 at 16:1 Comment(1)
Why not just use SSL on the remote PostgreSQL server, and connect with PostgreSQL's SSL support?Rudin
T
-1

For the moment I am using a solution bsed on this gist:

class SSHTunnel(object):
    """
    A context manager implementation of an ssh tunnel opened from python

    """


    def __init__(self, tunnel_command):

        assert "-fN" in tunnel_command, "need to open the tunnel with -fN"
        self._tunnel_command = tunnel_command
        self._delay = 0.1

    def create_tunnel(self):

        tunnel_cmd = self._tunnel_command
        import time, psutil, subprocess
        ssh_process = subprocess.Popen(tunnel_cmd,  universal_newlines=True,
                                                    shell=True,
                                                    stdout=subprocess.PIPE,
                                                    stderr=subprocess.STDOUT,
                                                    stdin=subprocess.PIPE)

        # Assuming that the tunnel command has "-f" and "ExitOnForwardFailure=yes", then the
        # command will return immediately so we can check the return status with a poll().

        while True:
            p = ssh_process.poll()
            if p is not None: break
            time.sleep(self._delay)


        if p == 0:
            # Unfortunately there is no direct way to get the pid of the spawned ssh process, so we'll find it
            # by finding a matching process using psutil.

            current_username = psutil.Process(os.getpid()).username
            ssh_processes = [proc for proc in psutil.get_process_list() if proc.cmdline == tunnel_cmd.split() and proc.username == current_username]

            if len(ssh_processes) == 1:
                self.ssh_tunnel = ssh_processes[0]
                return ssh_processes[0]
            else:
                raise RuntimeError, 'multiple (or zero?) tunnel ssh processes found: ' + str(ssh_processes)
        else:
            raise RuntimeError, 'Error creating tunnel: ' + str(p) + ' :: ' + str(ssh_process.stdout.readlines())


    def release(self):
        """ Get rid of the tunnel by killin the pid
        """
        self.ssh_tunnel.terminate()


    def __enter__(self):
        self.create_tunnel()
        return self


    def __exit__(self, type, value, traceback):

        self.release()


    def __del__(self):
        self.release()


def test():
    #do things that will fail if the tunnel is not opened

    print "done =========="


command = "ssh [email protected] -L %d:localhost:%d -p 222 -fN" % (someport, someport)

with SSHTunnel(command):
    test()

Please let me know if anybody has a better idea

Treadle answered 26/2, 2014 at 19:17 Comment(1)
Isn't there an error in the script? ssh_tunnel hasn't been defined before.Gigot
U
23

You could also use sshtunnel, short and sweet:

from sshtunnel import SSHTunnelForwarder

PORT=5432
with SSHTunnelForwarder((REMOTE_HOST, REMOTE_SSH_PORT),
         ssh_username=REMOTE_USERNAME,
         ssh_password=REMOTE_PASSWORD,
         remote_bind_address=('localhost', PORT),
         local_bind_address=('localhost', PORT)):
    conn = psycopg2.connect(...)
Used answered 23/10, 2015 at 10:6 Comment(3)
Can you give an example of using private key with passphrase? I can't see an example anywhere.Scirrhous
@rishabh-sagar, use ssh_pkey and ssh_private_key_password keyword arguments as documented in sshtunnel.readthedocs.org/en/latest. E.g. SSHTunnelForwarder(..., ssh_pkey='/path/to/keyfile', ssh_private_key_password='secret', ...)Used
The current import syntax is from sshtunnel import SSHTunnelForwarderMerkle
R
5

With sshtunnel package

I was not familiar with SSH tunnels, so I had some difficulties to use mrts's answer. Maybe thoses precisions could help someone.

In psycopg2.connect(), host and port are the one you just created by connecting remote host by ssh tunnel.

Here is my code :

from sshtunnel import SSHTunnelForwarder

server = SSHTunnelForwarder((REMOTE_HOST, REMOTE_SSH_PORT),
         ssh_username=REMOTE_USERNAME,
         ssh_password=REMOTE_PASSWORD,
         remote_bind_address=('localhost', PORT),
         local_bind_address=('localhost', PORT))
server.start()

import psycopg2
conn = psycopg2.connect(
    database=DATABASE,
    user=USER,
    host=server.local_bind_host,
    port=server.local_bind_port,
    password=PWD)
cur = conn.cursor()
cur.execute("select * from yourtable limit 1;")
data = cur.fetchall()
print(data)

I hope this example make it clearer.

Roshelle answered 2/6, 2021 at 14:5 Comment(0)
R
1

Call your ssh via os.system in a separate thread/process. You can also use -N with ssh to avoid opening a remote shell.

Rubetta answered 26/2, 2014 at 18:28 Comment(2)
Unfortunately this solution sometimes works sometimes does not depending on the time to create the tunnelTreadle
This is a normal race condition. Run ssh with -v using the subprocess module instead of os.system and wait for the ssh output when it says "Local forwarding listening" in the output. Use a threading.Event to synchronize the caller with the callee.Rubetta
B
0

Clodoaldo Neto's code worked for me perfectly but beware it doesn't clean up the process afterward.

The method shown by Luca Fiaschi also works for me. I updated it a bit for python3 and the updated psutil module. The changes were just that process.username and process.cmdline are now functions and that the iterator is process_iter() instead of get_process_list().

Here is an example of a very slightly modified version of the code Luca Fiaschi posted that works with python3 (requires psutil module). I hope it is at least mostly correct!

#!/usr/bin/env python3

import psutil
import psycopg2
import subprocess
import time
import os

# Tunnel Config
SSH_HOST = "111.222.333.444"
SSH_USER = "user"
SSH_KEYFILE = "key.pem"
SSH_FOREIGN_PORT = 5432   # Port that postgres is running on the foreign server
SSH_INTERNAL_PORT = 5432  # Port we open locally that is forwarded to
                          # FOREIGN_PORT on the server.

# Postgres Config
DB_HOST = "127.0.0.1"
DB_PORT = SSH_INTERNAL_PORT
DB_PASSWORD = "password"
DB_DATABASE = "postgres"
DB_USER = "user"

class SSHTunnel(object):
    """
    A context manager implementation of an ssh tunnel opened from python

    """
    def __init__(self, tunnel_command):
        assert "-fN" in tunnel_command, "need to open the tunnel with -fN"
        self._tunnel_command = tunnel_command
        self._delay = 0.1
        self.ssh_tunnel = None

    def create_tunnel(self):
        tunnel_cmd = self._tunnel_command
        ssh_process = subprocess.Popen(tunnel_cmd, universal_newlines=True,
            shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            stdin=subprocess.PIPE)

        # Assuming that the tunnel command has "-f" and "ExitOnForwardFailure=yes", then the
        # command will return immediately so we can check the return status with a poll().

        while True:
            p = ssh_process.poll()
            if p is not None: break
            time.sleep(self._delay)


        if p == 0:
            # Unfortunately there is no direct way to get the pid of the spawned ssh process, so we'll find it
            # by finding a matching process using psutil.

            current_username = psutil.Process(os.getpid()).username()
            ssh_processes = [proc for proc in psutil.process_iter() if proc.cmdline() == tunnel_cmd.split() and proc.username() == current_username]

            if len(ssh_processes) == 1:
                self.ssh_tunnel = ssh_processes[0]
                return ssh_processes[0]
            else:
                raise RuntimeError('multiple (or zero?) tunnel ssh processes found: ' + str(ssh_processes))
        else:
            raise RuntimeError('Error creating tunnel: ' + str(p) + ' :: ' + str(ssh_process.stdout.readlines()))

    def release(self):
        """ Get rid of the tunnel by killin the pid
        """
        if self.ssh_tunnel:
            self.ssh_tunnel.terminate()

    def __enter__(self):
        self.create_tunnel()
        return self

    def __exit__(self, type, value, traceback):
        self.release()

    def __del__(self):
        self.release()

command = "ssh -i %s %s@%s -fNL %d:localhost:%d"\
    % (SSH_KEYFILE, SSH_USER, SSH_HOST, SSH_INTERNAL_PORT, SSH_FOREIGN_PORT)

with SSHTunnel(command):
    conn = psycopg2.connect(host = DB_HOST, password = DB_PASSWORD,
                     database = DB_DATABASE, user = DB_USER, port = DB_PORT)
    curs = conn.cursor()
    sql = "select * from table"
    curs.execute(sql)
    rows = curs.fetchall()
    print(rows)
Baxley answered 9/10, 2014 at 23:17 Comment(2)
I get an attribute error when I try to execute the script. "SSHTunnel" has no attribute 'ssh_tunnel'. Please help.Gigot
It looks like the only time self.ssh_tunnel is accessed is in SSHTunnel.release. If that variable doesn't exist, I bet self.ssh_tunnel isn't getting set which means ssh_processes isn't filling with any processes. How are you invoking ssh (the variable "command") and does invoking that command by itself in a shell execute? Also, I'll edit the script to make it not error.Baxley
K
0

Connect to AWS/GCP/Azure Through Bastion Tunnel

I got very confused about where to put TUNNEL credentials and where to put AWS/GCP/Azure bits, so here is a working example (using with statements for better maintainability).

import os

import psycopg2
from dotenv import load_dotenv
from sshtunnel import SSHTunnelForwarder

load_dotenv()

# Setting up the SSH tunnel with tunnel credentials
with SSHTunnelForwarder(
    ssh_address_or_host=(os.environ.get("TUNNEL_HOST"), int(os.environ.get("TUNNEL_PORT"))),
    ssh_username=os.environ.get("TUNNEL_USERNAME"),
    ssh_pkey=os.environ.get("TUNNEL_PRIVATE_KEY"),
    # Key part! Connect to AWS_HOST through the tunnel.
    remote_bind_address=(os.environ.get("AWS_HOST"), int(os.environ.get("AWS_PORT"))),
    # The port doesn't matter, but pick AWS_PORT for ease.
    local_bind_address=("localhost", int(os.environ.get("AWS_PORT")))
) as server:
    # Connect to the database using AWS credentials
    with psycopg2.connect(
        database=os.environ.get("AWS_DATABASE"),
        user=os.environ.get("AWS_USER"),
        password=os.environ.get("AWS_PASSWORD"),
        # Key part! You connect through the server!
        host=server.local_bind_host,
        port=server.local_bind_port,
    ) as conn:
        # Run SQL queries
        with conn.cursor() as cur:
            cur.execute("SELECT * FROM table LIMIT 2;")
            data = cur.fetchall()
            print(data)

Some notes:

  • I'm using python-dotenv library since you'll usually store credentials in a .env file.
  • Assuming you use a private key to connect to the tunnel and password to connect to AWS. You can change ssh_pkey to ssh_password in SSHTunnelForwarder and password to sslkey in psycopg2.connect if you require something different.
  • You must do int(os.environ.get("TUNNEL_PORT")) because everything in .env files is a string.
  • data is a list of tuples
Kamp answered 16/1 at 11:50 Comment(0)
T
-1

For the moment I am using a solution bsed on this gist:

class SSHTunnel(object):
    """
    A context manager implementation of an ssh tunnel opened from python

    """


    def __init__(self, tunnel_command):

        assert "-fN" in tunnel_command, "need to open the tunnel with -fN"
        self._tunnel_command = tunnel_command
        self._delay = 0.1

    def create_tunnel(self):

        tunnel_cmd = self._tunnel_command
        import time, psutil, subprocess
        ssh_process = subprocess.Popen(tunnel_cmd,  universal_newlines=True,
                                                    shell=True,
                                                    stdout=subprocess.PIPE,
                                                    stderr=subprocess.STDOUT,
                                                    stdin=subprocess.PIPE)

        # Assuming that the tunnel command has "-f" and "ExitOnForwardFailure=yes", then the
        # command will return immediately so we can check the return status with a poll().

        while True:
            p = ssh_process.poll()
            if p is not None: break
            time.sleep(self._delay)


        if p == 0:
            # Unfortunately there is no direct way to get the pid of the spawned ssh process, so we'll find it
            # by finding a matching process using psutil.

            current_username = psutil.Process(os.getpid()).username
            ssh_processes = [proc for proc in psutil.get_process_list() if proc.cmdline == tunnel_cmd.split() and proc.username == current_username]

            if len(ssh_processes) == 1:
                self.ssh_tunnel = ssh_processes[0]
                return ssh_processes[0]
            else:
                raise RuntimeError, 'multiple (or zero?) tunnel ssh processes found: ' + str(ssh_processes)
        else:
            raise RuntimeError, 'Error creating tunnel: ' + str(p) + ' :: ' + str(ssh_process.stdout.readlines())


    def release(self):
        """ Get rid of the tunnel by killin the pid
        """
        self.ssh_tunnel.terminate()


    def __enter__(self):
        self.create_tunnel()
        return self


    def __exit__(self, type, value, traceback):

        self.release()


    def __del__(self):
        self.release()


def test():
    #do things that will fail if the tunnel is not opened

    print "done =========="


command = "ssh [email protected] -L %d:localhost:%d -p 222 -fN" % (someport, someport)

with SSHTunnel(command):
    test()

Please let me know if anybody has a better idea

Treadle answered 26/2, 2014 at 19:17 Comment(1)
Isn't there an error in the script? ssh_tunnel hasn't been defined before.Gigot
A
-3
from time import sleep

os.system("ssh [email protected] -fNL 5432:localhost:5432 -p 222")

while True:
    try:
        conn = psycopg2.connect(
            "host=localhost dbname={0} user={1} password={2}".format(
                conf.dbname, conf.user, conf.password
            )
        )
        break
    except psycopg2.OperationalError:
        sleep(3)
Allopath answered 26/2, 2014 at 23:54 Comment(3)
Does not execute or give back any input from the "conn = psycopg2.connect()" part.Boeke
@SirBenBenji There is nothing to be executed in this answer. The only thing it does is to establish an ssh connection and connect to the database as asked. What were you expecting?Allopath
It does not connect to the database on my system, or I am geting no feedback from your solution. Cursor just blinks and we are "caught" in "-N". I went with subprocess. os.system can be considered obsolete.Boeke

© 2022 - 2024 — McMap. All rights reserved.