Ideally, you should construct this in a manner that doesn't expose you to the problem at all -- which is to say, let the Python interpreter do the work of generating a correctly shell-quoted string (and then re-quoting it to be safely passed to ssh).
try:
from shlex import quote # Python 3
except ImportError:
from pipes import quote # Python 2
import subprocess
# specify your commands the way they're actually seen by the operating system -- with
# lists of strings as argument vectors.
rmt_pipeline = [[ 'lastlog', '-u', 'ubuntu' ],
[ 'grep', '-v', 'Latest' ],
[ 'awk', '{$1="";$2="";$3="";print $0 }' ]]
# ...then, let shlex.quote() or pipes.quote() determine how to make those lists be valid
# shell syntax, as expected by the remote copy of sh -c '...' invoked by ssh
rmt_pipeline_str = ' | '.join(' '.join(quote(word) for word in piece)
for piece in rmt_pipeline)
# ...finally, generate the argument vector for our local copy of ssh...
ssh_cmd = [ 'ssh', '-i', '/Users/abcxyz/keypair', rmt_pipeline_str ]
# and actually invoke it.
user_output = subprocess.Popen(ssh_cmd, stdout=subprocess.PIPE).stdout
If you really want to use os.popen() -- which you shouldn't, as Python documentation explicitly suggests using subprocess instead -- you can replace the last line with:
ssh_cmd_str = ' '.join(quote(word) for word in ssh_cmd)
user_output = os.popen(ssh_cmd_str)
On UNIX-family operating systems, all program execution happens through the execve() syscall, which passes a list of C strings around. Specifying that list yourself gives you the most possible control over how execution takes place, and prevents shell injection attacks (where a user authorized to provide a parameter to one of the programs you're running passes content that's interpreted by the shell as syntax rather than data, and thus runs a completely different program or an indirection operation instead).