Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
.cache
.coverage
.tox
.envrc
build
dist
venv
238 changes: 162 additions & 76 deletions geofrontcli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import logging
import os
import os.path
import pprint
import subprocess
import sys
import time
import webbrowser

from dirspec.basedir import load_config_paths, save_config_path
Expand All @@ -21,6 +23,7 @@
NoTokenIdError, ProtocolVersionError, RemoteError,
TokenIdError, UnfinishedAuthenticationError)
from .key import PublicKey
from .utils import resolve_cmdarg_template
from .version import VERSION


Expand Down Expand Up @@ -54,6 +57,8 @@
version='%(prog)s ' + VERSION)
subparsers = parser.add_subparsers()

logger = logging.getLogger('geofrontcli')


def get_server_url():
for path in load_config_paths(CONFIG_RESOURCE):
Expand Down Expand Up @@ -120,13 +125,17 @@ def authenticate(args):
"""Authenticate to Geofront server."""
client = get_client()
while True:
with client.authenticate() as url:
if args.open_browser:
print('Continue to authenticate in your web browser...')
webbrowser.open(url)
else:
print('Continue to authenticate in your web browser:')
print(url)
try:
with client.authenticate() as url:
if args.open_browser:
print('Continue to authenticate in your web browser...')
webbrowser.open(url)
else:
print('Continue to authenticate in your web browser:')
print(url)
except Exception as e:
# exception info is already provided in client.Client.authenticate()
return
input('Press return to continue')
try:
client.identity
Expand Down Expand Up @@ -205,18 +214,32 @@ def masterkey(args):


def align_remote_list(remotes):
maxlength = max(map(len, remotes)) if remotes else 0
if remotes:
maxlen_alias = max(map(len, remotes.keys()))
maxlen_user = max(map(lambda v: len(v['user']), remotes.values()))
maxlen_host = max(map(lambda v: len(v['host']), remotes.values()))
else:
maxlen_alias = 1
maxlen_user = 1
maxlen_host = 1
for alias, remote in sorted(remotes.items()):
if remote.endswith(':22'):
remote = remote[:-3]
yield '{0:{1}} {2}'.format(alias, maxlength, remote)
yield '{0:{1}} {2:{3}} @ {4:{5}} : {6}'.format(
alias, maxlen_alias,
remote['user'], maxlen_user,
remote['host'], maxlen_host,
remote['port'])


@subparser
def remotes(args):
"""List available remotes."""
client = get_client()
remotes = client.remotes
try:
remotes = client.remotes
except Exception:
# exception info is already provided in client.Client.remotes()
return
time.sleep(0.11)
if args.alias:
for alias in sorted(remotes):
print(alias)
Expand All @@ -233,6 +256,21 @@ def remotes(args):
)


@subparser
def remote(args):
"""Get the information of a specific remote."""
client = get_client()
remote = client.remote(args.remote)
time.sleep(0.11)
pprint.pprint(remote)


remote.add_argument(
'remote',
help='the remote alias that you want to get information about'
)


@subparser
def authorize(args, alias=None):
"""Temporarily authorize you to access the given remote.
Expand Down Expand Up @@ -261,30 +299,14 @@ def authorize(args, alias=None):
)


def get_ssh_options(remote):
def mangle_ssh_args(remote):
"""Translate the given ``remote`` to a corresponding :program:`ssh`
options. For example, it returns the following list for ``'user@host'``::

['-l', 'user', 'host']

The remote can contain the port number or omit the user login as well
e.g. ``'host:22'``::

['-p', '22', 'host']

"""
remote_match = REMOTE_PATTERN.match(remote)
if not remote_match:
raise ValueError('invalid remote format: ' + str(remote))
options = []
user = remote_match.group('user')
if user:
options.extend(['-l', user])
port = remote_match.group('port')
if port:
options.extend(['-p', port])
options.append(remote_match.group('host'))
return options
arguments including the login name and the port number explicitly."""
return [
'-l', remote['user'],
'-p', str(remote['port']),
remote['host'],
]


@subparser
Expand All @@ -295,15 +317,15 @@ def colonize(args):

"""
client = get_client()
remote = client.remotes.get(args.remote, args.remote)
try:
options = get_ssh_options(remote)
except ValueError as e:
colonize.error(str(e))
remote = client.remotes.get(args.remote, args.remote)
except:
# exception info is already provided in client.Client.remote()
return
cmd = [args.ssh]
if args.identity_file:
cmd.extend(['-i', args.identity_file])
cmd.extend(options)
cmd.extend(mangle_ssh_args(remote))
cmd.extend([
'mkdir', '~/.ssh', '&>', '/dev/null', '||', 'true', ';',
'echo', repr(str(client.master_key)),
Expand All @@ -322,62 +344,115 @@ def colonize(args):


@subparser
def ssh(args, alias=None):
def ssh(args):
"""SSH to the remote through Geofront's temporary authorization."""
remote = authorize.call(args, alias=alias)
if args.tunnel and sys.version_info < (3, 6):
logger.error('To use the SSH proxy, you need to run geofront-cli on '
'Python 3.6 or higher.',
extra={'user_waiting': False})
return
remote_match = REMOTE_PATTERN.match(args.remote)
if not remote_match:
raise ValueError('invalid remote format: ' + str(args.remote))
alias = remote_match.group('host')
user = remote_match.group('user')
# port from remote_match is ignored
client = get_client()
try:
options = get_ssh_options(remote)
except ValueError as e:
ssh.error(str(e))
subprocess.call([args.ssh] + options)
remote = client.remote(alias, quiet=True)
except Exception:
# exception info is already provided in client.Client.remote()
return
if user and user != remote['user']:
remote['user'] = user # override username
else:
remote = authorize.call(args, alias=alias)
template = [
args.ssh,
'-l', '$user',
'-p', '$port',
]
if args.identity:
template.extend(['-i', args.identity])
if args.dynamic_port:
template.extend(['-D', args.dynamic_port])
template.append('$host')
if args.tunnel:
client.ssh_proxy(template, remote, alias or args.remote)
else:
cmdargs = resolve_cmdarg_template(template, remote)
subprocess.call(cmdargs)


ssh.add_argument('remote', help='the remote alias to ssh')
ssh.add_argument('-i', '--identity',
help='alternative SSH identity (private key)')
ssh.add_argument('-D', '--dynamic-port',
help='port number to use for dynamic TCP forwarding')
ssh.add_argument('-t', '--tunnel', action='store_true', default=False,
help='use SSH tunneling via HTTPS WebSockets to access'
'servers inside remote private networks')


def parse_scp_path(path, args):
"""Parse remote:path format."""
if ':' not in path:
return None, path
alias, path = path.split(':', 1)
remote = authorize.call(args, alias=alias)
return remote, path
host, path = path.split(':', 1)
return host, path


@subparser
def scp(args):
options = []
src_remote, src_path = parse_scp_path(args.source, args)
dst_remote, dst_path = parse_scp_path(args.destination, args)
if src_remote and dst_remote:
"""SCP from/to the remote through Geofront's temporary authorization."""
if args.tunnel and sys.version_info < (3, 6):
logger.error('To use the SSH proxy, you need to run geofront-cli on '
'Python 3.6 or higher.',
extra={'user_waiting': False})
template = [args.scp]
src_host, src_path = parse_scp_path(args.source, args)
dst_host, dst_path = parse_scp_path(args.destination, args)
if src_host and dst_host:
scp.error('source and destination cannot be both '
'remote paths at a time')
elif not (src_remote or dst_remote):
elif not (src_host or dst_host):
scp.error('one of source and destination has to be a remote path')
if args.ssh:
options.extend(['-S', args.ssh])
template.extend(['-S', args.ssh])
if args.recursive:
options.append('-r')
remote = src_remote or dst_remote
remote_match = REMOTE_PATTERN.match(remote)
if not remote_match:
raise ValueError('invalid remote format: ' + str(remote))
port = remote_match.group('port')
if port:
options.extend(['-P', port])
host = remote_match.group('host')
user = remote_match.group('user')
if user:
host = user + '@' + host
if src_remote:
options.append(host + ':' + src_path)
template.append('-r')
if args.identity:
template.extend(['-i', args.identity])
host = src_host or dst_host
host_match = REMOTE_PATTERN.match(host)
if not host_match:
raise ValueError('invalid remote format: ' + str(host))
alias = host_match.group('host')
user = host_match.group('user')
# port from host_match is ignored
template.extend(['-P', '$port'])
if src_host:
template.append('$user@$host:' + src_path)
else:
options.append(src_path)
if dst_remote:
options.append(host + ':' + dst_path)
template.append(src_path)
if dst_host:
template.append('$user@$host:' + dst_path)
else:
options.append(dst_path)
subprocess.call([args.scp] + options)
template.append(dst_path)
client = get_client()
remote = client.remote(alias, quiet=True)
if user and user != remote_info['user']:
remote['user'] = user # override username
else:
remote = authorize.call(args, alias=alias)
if args.tunnel:
client.ssh_proxy(template, remote, alias)
else:
subprocess.call(resolve_cmdarg_template(template, {
'host': remote['host'],
'user': remote['user'],
'port': remote['port'],
}))


scp.add_argument(
Expand All @@ -388,9 +463,14 @@ def scp(args):
)
scp.add_argument(
'-r', '-R', '--recursive',
action='store_true',
action='store_true', default=False,
help='recursively copy entire directories'
)
scp.add_argument('-i', '--identity',
help='alternative SSH identity (private key)')
scp.add_argument('-t', '--tunnel', action='store_true', default=False,
help='use SSH tunneling via HTTPS WebSockets to access'
'servers inside remote private networks')
scp.add_argument('source', help='the source path to copy')
scp.add_argument('destination', help='the destination path')

Expand All @@ -399,7 +479,11 @@ def scp(args):
def go(args):
"""Select a remote and SSH to it at once (in interactive way)."""
client = get_client()
remotes = client.remotes
try:
remotes = client.remotes
except Exception:
# exception info is already provided in client.Client.remotes()
return
chosen = iterfzf(align_remote_list(remotes))
if chosen is None:
return
Expand Down Expand Up @@ -485,6 +569,8 @@ def main(args=None):
parser.exit('geofront-cli seems incompatible with the server.\n'
'Try `pip install --upgrade geofront-cli` command.\n'
'The server version is {0}.'.format(e.server_version))
except KeyboardInterrupt:
parser.exit('Aborted.')
else:
parser.print_usage()

Expand Down
Loading