Skip to content

Commit

Permalink
Implement export and import commands for ssh conf.
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim Beiner authored and Maxbey committed Mar 20, 2017
1 parent 3a73966 commit 4cacf43
Show file tree
Hide file tree
Showing 21 changed files with 352 additions and 208 deletions.
11 changes: 7 additions & 4 deletions contrib/completion/bash/termius
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ _termius()
_get_comp_words_by_ref -n : cur prev words

# Command data:
cmds='complete connect fullclean group groups help host hosts identities identity info init key keys login logout pfrule pfrules pull push snippet snippets sync tags settings'
cmds='complete connect fullclean group groups help host hosts identities identity info init key keys login logout pfrule pfrules pull push snippet snippets import-ssh-config export-ssh-config tags settings'
cmds_complete='-h --help --name --shell'
cmds_complete_arg_options='--name --shell'
cmds_settings='-h --help --synchronize-key --agent-forwarding --log-file'
Expand Down Expand Up @@ -265,9 +265,12 @@ _termius()
cmds_snippets='-h --help -f --format -c --column --max-width --noindent --quote --log-file'
cmds_snippets_arg_options='-f --format -c --column --max-width --log-file'
cmds_snippets_file_options='--log-file'
cmds_sync='-h --help --log-file -c --credentials'
cmds_sync_arg_options='--log-file -c --credentials'
cmds_sync_file_options='--log-file -c --credentials'
cmds_import-ssh-config='-h --help --log-file'
cmds_import-ssh-config_arg_options='--log-file'
cmds_import-ssh-config_file_options='--log-file'
cmds_export-ssh-config='-h --help --log-file'
cmds_export-ssh-config_arg_options='--log-file'
cmds_export-ssh-config_file_options='--log-file'
cmds_tags='-h --help -f --format -c --column --max-width --noindent --quote --log-file -d --delete'
cmds_tags_arg_options='-f --format -c --column --max-width --log-file'
cmds_tags_file_options='--log-file'
Expand Down
15 changes: 10 additions & 5 deletions contrib/completion/zsh/_termius
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ _termius() {
"push[Send local changes to serveraudtor.com.]" \
"snippet[Snippet operations.]" \
"snippets[List snippets.]" \
"sync[Sync IaaS or PaaS service hosts with termius.]" \
"import-ssh-config[Import hosts from user`s ssh config.]" \
"export-ssh-config[Export hosts from local storage to generated file.]" \
"tags[List tags.]" \
"settings[Configure application settings.]"
fi
Expand Down Expand Up @@ -357,13 +358,17 @@ _termius_snippets() {
'--column[The column(s) to include]:columns:()' \
'--quote[When to include quotes]:quote:(all minimal none nonnumeric)'
}
_termius_sync() {
_termius_import-ssh-config() {
_arguments \
'-h[Display help]' \
'--help[Display help]' \
'--log-file[Log to this file]:filename:_files' \
'-c[Credential file]:filename:_files' \
'--credentials[Credential file]:filename:_files'
'--log-file[Log to this file]:filename:_files'
}
_termius_export-ssh-config() {
_arguments \
'-h[Display help]' \
'--help[Display help]' \
'--log-file[Log to this file]:filename:_files'
}
_termius_tags() {
_arguments \
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@

# pylint: disable=invalid-name
handlers = [
'sync = termius.sync.commands:SyncCommand',
'import-ssh-config = termius.porting.commands:SSHImportCommand',
'export-ssh-config = termius.porting.commands:SSHExportCommand',
'login = termius.account.commands:LoginCommand',
'logout = termius.account.commands:LogoutCommand',
'settings = termius.account.commands:SettingsCommand',
Expand Down
4 changes: 2 additions & 2 deletions termius/handlers/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from cached_property import cached_property
from ..core.commands import DetailCommand, ListCommand
from ..core.commands.single import RequiredOptions
from ..core.commands.mixins import SshConfigPrepareMixin, GroupStackGetterMixin
from ..core.commands.mixins import GroupStackGetterMixin
from ..core.storage.strategies import RelatedGetStrategy
from ..core.models.terminal import Host, Group, TagHost
from .taghost import TagListArgs
Expand Down Expand Up @@ -75,7 +75,7 @@ def serialize_args(self, args, instance=None):
return instance


class HostsCommand(SshConfigPrepareMixin, GroupStackGetterMixin, ListCommand):
class HostsCommand(GroupStackGetterMixin, ListCommand):
"""Manage host objects."""

model_class = Host
Expand Down
15 changes: 7 additions & 8 deletions termius/handlers/init.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Module with init command."""

from argparse import Namespace
Expand All @@ -7,7 +8,7 @@
from termius.account.commands import LoginCommand
from termius.cloud.commands import PullCommand, PushCommand
from termius.core.commands import AbstractCommand
from termius.sync.commands import SyncCommand
from termius.porting.commands import SSHImportCommand


class InitCommand(AbstractCommand):
Expand All @@ -34,9 +35,7 @@ def init_namespace(self, parsed_args, username, password):
return Namespace(
log_file=parsed_args.log_file,
username=username,
password=password,
service='ssh',
credentials=None
password=password
)

def login(self, parsed_args):
Expand All @@ -49,9 +48,9 @@ def pull(self, parsed_args):
command = PullCommand(self.app, self.app_args, self.cmd_name)
command.take_action(parsed_args)

def sync_ssh(self, parsed_args):
def import_ssh(self, parsed_args):
"""Wrapper for sync command."""
command = SyncCommand(self.app, self.app_args, self.cmd_name)
command = SSHImportCommand(self.app, self.app_args, self.cmd_name)
command.take_action(parsed_args)

def push(self, parsed_args):
Expand All @@ -73,8 +72,8 @@ def take_action(self, parsed_args):
self.login(namespace)
self.log.info('\nPull your data from termius cloud...')
self.pull(namespace)
self.log.info('\nSync local storage with your ~/.ssh/config...')
self.sync_ssh(namespace)
self.log.info('\nImport ssh config...')
self.import_ssh(namespace)
self.log.info('\nPush local data to termius cloud...')
self.push(namespace)

Expand Down
File renamed without changes.
38 changes: 38 additions & 0 deletions termius/porting/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""Module with CLI command to export and import hosts."""
from termius.core.storage.strategies import (
RelatedSaveStrategy, RelatedGetStrategy
)

from termius.porting.providers.ssh.provider import SSHPortingProvider

from ..core.commands import AbstractCommand


class SSHImportCommand(AbstractCommand):
"""Import hosts from user`s ssh config."""

save_strategy = RelatedSaveStrategy
get_strategy = RelatedGetStrategy

def take_action(self, parsed_args):
"""Process CLI call."""
provider = SSHPortingProvider(storage=self.storage, crendetial=None)
provider.import_hosts()

self.log.info('Import hosts from ~/.ssh/config to local storage.')


class SSHExportCommand(AbstractCommand):
"""Export hosts from local storage to generated file."""

get_strategy = RelatedGetStrategy

def take_action(self, parsed_args):
"""Process CLI call."""
provider = SSHPortingProvider(storage=self.storage, crendetial=None)
provider.export_hosts()

self.log.info(
'Export hosts from local storage to ~/.termius/sshconfig'
)
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,36 @@
"""Acquire SaaS and IaaS hosts."""
import abc
import six

from ...core.commands.mixins import SshConfigMergerMixin
from ...core.models.terminal import Host, SshKey


@six.add_metaclass(abc.ABCMeta)
class BaseSyncService(object):
class BasePortingProvider(SshConfigMergerMixin):
"""Base class for acquiring SaaS and IaaS hosts into storage."""

def __init__(self, storage, crendetial):
"""Construct new instance for providing hosts from SaaS and IaaS."""
self.crendetial = crendetial
self.storage = storage
self.crendetial = crendetial

@abc.abstractmethod
def hosts(self):
"""Override to return host instances."""
def provider_hosts(self):
"""Override to return host instances from provider."""

@abc.abstractmethod
def export_hosts(self):
"""Export hosts to provider format."""

def import_hosts(self):
"""Import hosts from provider and save it in local storage."""
hosts_to_import = self.provider_hosts()

def sync(self):
"""Sync storage content and the Service hosts."""
service_hosts = self.hosts()
with self.storage:
for i in service_hosts:
updated_i = self.assign_existed_host_ids(i)
self.storage.save(updated_i)

def assign_existed_host_ids(self, new_host):
"""Assign to new host existed host id to update it."""
existed_host = self.get_existed_host(new_host)
if not existed_host:
return new_host
new_host.id = existed_host.id
new_host.ssh_config.id = existed_host.ssh_config.id
existed_identity = existed_host.ssh_config.identity
if not (existed_identity and existed_identity.is_visible):
new_host.ssh_config.identity.id = existed_identity.id
if new_host.ssh_config.identity.ssh_key:
self.assign_ssh_key_ids(new_host.ssh_config.identity.ssh_key)

return new_host
for host in hosts_to_import:
if not self.get_existed_host(host):
self.storage.save(host)

def assign_ssh_key_ids(self, new_ssh_key):
"""Assign to new ssh key existed ssh key id to update it."""
Expand Down
2 changes: 2 additions & 0 deletions termius/porting/providers/ssh/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Package with logic to import and export hosts from ssh config provider."""
83 changes: 83 additions & 0 deletions termius/porting/providers/ssh/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""Module with adapter for ssh config hosts."""
from os import environ

from pathlib2 import Path

from termius.core.commands.mixins import SshConfigMergerMixin
from termius.core.models.terminal import Host, SshConfig, Identity, SshKey


class SSHConfigHostAdapter(SshConfigMergerMixin):
"""Class for adapting app host and ssh config hosts."""

default_user = environ.get('USER', None)

def get_instance_ssh_key_label(self, ssh_config):
"""Helper to retrieve ssh_key lable."""
if ssh_config['identity'] and ssh_config['identity']['ssh_key']:
return ssh_config['identity']['ssh_key']['label']

return None

def create_key(self, config):
"""Construct new application ssh key instance."""
if 'identityfile' not in config:
return None
identityfile = self.choose_ssh_key(config['identityfile'], config)
if not identityfile:
return None
content = identityfile.read_text()
return SshKey(label=identityfile.name, private_key=content)

# pylint: disable=unused-argument,no-self-use
def choose_ssh_key(self, sshkeys, host_config):
"""Choose single ssh key path instance from ones."""
key_paths = [Path(i) for i in sshkeys]
existed_paths = [i for i in key_paths if i.is_file()]
return existed_paths and existed_paths[0]

def adapt_instance_to_ssh_config_host(self, host_instance):
"""Convert app host to ssh config host."""
host_instance.ssh_config.identity.is_visible = True
ssh_config = self.get_merged_ssh_config(host_instance)

adapted = {
'hostname': host_instance['address'],
'user': ssh_config['identity']['username'],
'port': ssh_config['port'] or 22
}

host_key_label = self.get_instance_ssh_key_label(ssh_config)

if host_key_label:
adapted.update(
identityfile='~/.termius/ssh_keys/' + host_key_label
)

return adapted

def adapt_ssh_config_host_to_instance(self, alias, parsed_host):
"""Convert parsed host to application host."""
app_host = Host(
label=alias,
address=parsed_host['hostname'],
)
ssh_config = SshConfig(
identity=Identity(
username=parsed_host.get('user', self.default_user),
ssh_key=self.create_key(parsed_host)
)
)

ssh_config.port = parsed_host.get('port')
ssh_config.timeout = parsed_host.get('serveraliveinterval')
ssh_config.keep_alive_packages = parsed_host.get('serveralivecountmax')
ssh_config.use_ssh_key = parsed_host.get('identitiesonly')
ssh_config.strict_host_key_check = parsed_host.get(
'stricthostkeychecking'
)

app_host.ssh_config = ssh_config

return app_host
60 changes: 60 additions & 0 deletions termius/porting/providers/ssh/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""Module with ssh config parser."""

import re
from paramiko.config import SSHConfig


class SSHConfigParser(SSHConfig):
"""Class to override parse method of paramiko parser."""

def parse(self, file_obj): # noqa
"""
Read an OpenSSH config from the given file object.
:param file_obj: a file-like object to read the config file from
"""
termius_ignore_regexp = re.compile(r'# termius:ignore')

host = {'host': ['*'], 'config': {}}

for line in file_obj:
line = line.strip()

if not line:
continue

if line.startswith('#'):
ignore_comment = termius_ignore_regexp.match(line)

if ignore_comment:
host['config']['ignore'] = ''

continue

match = re.match(self.SETTINGS_REGEX, line)
if not match:
raise Exception('Unparsable line %s' % line)
key = match.group(1).lower()
value = match.group(2)
if key == 'host':
self._config.append(host)
host = {
'host': self._get_hosts(value),
'config': {}
}

elif key == 'proxycommand' and value.lower() == 'none':
host['config'][key] = None
else:
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
if key in ['identityfile', 'localforward', 'remoteforward']:
if key in host['config']:
host['config'][key].append(value)
else:
host['config'][key] = [value]
elif key not in host['config']:
host['config'][key] = value

self._config.append(host)
Loading

0 comments on commit 4cacf43

Please sign in to comment.