From 4cacf43e58b332d37be50580426f3aa975121e8b Mon Sep 17 00:00:00 2001 From: Maxim Beiner Date: Fri, 17 Mar 2017 16:25:00 +0600 Subject: [PATCH] Implement export and import commands for ssh conf. --- contrib/completion/bash/termius | 11 ++- contrib/completion/zsh/_termius | 15 ++-- setup.py | 3 +- termius/handlers/host.py | 4 +- termius/handlers/init.py | 15 ++-- termius/{sync => porting}/__init__.py | 0 termius/porting/commands.py | 38 +++++++++ .../{sync => porting}/providers/__init__.py | 0 termius/{sync => porting}/providers/base.py | 42 ++++------ termius/porting/providers/ssh/__init__.py | 2 + termius/porting/providers/ssh/adapter.py | 83 +++++++++++++++++++ termius/porting/providers/ssh/parser.py | 60 ++++++++++++++ termius/porting/providers/ssh/provider.py | 82 ++++++++++++++++++ termius/sync/commands.py | 52 ------------ termius/sync/providers/ssh.py | 76 ----------------- tests/integration/export-ssh-config.bats | 11 +++ tests/integration/import-ssh-config.bats | 11 +++ tests/integration/sync.bats | 17 ---- tests/unit/{sync => porting}/__init__.py | 0 .../{sync => porting}/providers/__init__.py | 0 .../{sync => porting}/providers/ssh_test.py | 38 +++++---- 21 files changed, 352 insertions(+), 208 deletions(-) rename termius/{sync => porting}/__init__.py (100%) create mode 100644 termius/porting/commands.py rename termius/{sync => porting}/providers/__init__.py (100%) rename termius/{sync => porting}/providers/base.py (54%) create mode 100644 termius/porting/providers/ssh/__init__.py create mode 100644 termius/porting/providers/ssh/adapter.py create mode 100644 termius/porting/providers/ssh/parser.py create mode 100644 termius/porting/providers/ssh/provider.py delete mode 100644 termius/sync/commands.py delete mode 100644 termius/sync/providers/ssh.py create mode 100644 tests/integration/export-ssh-config.bats create mode 100644 tests/integration/import-ssh-config.bats delete mode 100644 tests/integration/sync.bats rename tests/unit/{sync => porting}/__init__.py (100%) rename tests/unit/{sync => porting}/providers/__init__.py (100%) rename tests/unit/{sync => porting}/providers/ssh_test.py (77%) diff --git a/contrib/completion/bash/termius b/contrib/completion/bash/termius index 6ad4d26..00be20a 100644 --- a/contrib/completion/bash/termius +++ b/contrib/completion/bash/termius @@ -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' @@ -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' diff --git a/contrib/completion/zsh/_termius b/contrib/completion/zsh/_termius index 6c63b9e..c40e1c6 100644 --- a/contrib/completion/zsh/_termius +++ b/contrib/completion/zsh/_termius @@ -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 @@ -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 \ diff --git a/setup.py b/setup.py index 6398358..94c82ee 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/termius/handlers/host.py b/termius/handlers/host.py index 0f78652..c2945b4 100644 --- a/termius/handlers/host.py +++ b/termius/handlers/host.py @@ -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 @@ -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 diff --git a/termius/handlers/init.py b/termius/handlers/init.py index f6d0437..6b0b22c 100644 --- a/termius/handlers/init.py +++ b/termius/handlers/init.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Module with init command.""" from argparse import Namespace @@ -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): @@ -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): @@ -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): @@ -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) diff --git a/termius/sync/__init__.py b/termius/porting/__init__.py similarity index 100% rename from termius/sync/__init__.py rename to termius/porting/__init__.py diff --git a/termius/porting/commands.py b/termius/porting/commands.py new file mode 100644 index 0000000..a1b988d --- /dev/null +++ b/termius/porting/commands.py @@ -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' + ) diff --git a/termius/sync/providers/__init__.py b/termius/porting/providers/__init__.py similarity index 100% rename from termius/sync/providers/__init__.py rename to termius/porting/providers/__init__.py diff --git a/termius/sync/providers/base.py b/termius/porting/providers/base.py similarity index 54% rename from termius/sync/providers/base.py rename to termius/porting/providers/base.py index c24450c..4200dde 100644 --- a/termius/sync/providers/base.py +++ b/termius/porting/providers/base.py @@ -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.""" diff --git a/termius/porting/providers/ssh/__init__.py b/termius/porting/providers/ssh/__init__.py new file mode 100644 index 0000000..ad30a8e --- /dev/null +++ b/termius/porting/providers/ssh/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Package with logic to import and export hosts from ssh config provider.""" diff --git a/termius/porting/providers/ssh/adapter.py b/termius/porting/providers/ssh/adapter.py new file mode 100644 index 0000000..1b3e5ee --- /dev/null +++ b/termius/porting/providers/ssh/adapter.py @@ -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 diff --git a/termius/porting/providers/ssh/parser.py b/termius/porting/providers/ssh/parser.py new file mode 100644 index 0000000..8b6a274 --- /dev/null +++ b/termius/porting/providers/ssh/parser.py @@ -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) diff --git a/termius/porting/providers/ssh/provider.py b/termius/porting/providers/ssh/provider.py new file mode 100644 index 0000000..780b2a6 --- /dev/null +++ b/termius/porting/providers/ssh/provider.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +"""Module with ssh sync provider.""" +import re + +from os.path import expanduser +from pathlib2 import Path + +from termius.core.models.terminal import Host + +from ..base import BasePortingProvider +from .parser import SSHConfigParser +from .adapter import SSHConfigHostAdapter + + +class SSHPortingProvider(BasePortingProvider): + """Synchronize ssh config content with application.""" + + user_config = '~/.ssh/config' + export_path = expanduser('~/.termius/sshconfig') + + # pylint: disable=anomalous-backslash-in-string + allowed_host_re = re.compile('[?*\[\]]') + default_port = 22 + + def __init__(self, *args, **kwargs): + """Contruct new service to sync ssh config.""" + super(SSHPortingProvider, self).__init__(*args, **kwargs) + self.user_config = expanduser(self.user_config) + self.adapter = SSHConfigHostAdapter() + + def export_hosts(self): + """Export app hosts to ssh config syntax.""" + hosts_in_storage = self.storage.get_all(Host) + with Path(self.export_path).open(mode='w+') as export_file: + for host in hosts_in_storage: + self.export_host( + export_file, + host['label'] or host['address'], + self.adapter.adapt_instance_to_ssh_config_host(host) + ) + + def provider_hosts(self): + """Retrieve host instances from ssh config.""" + parser = SSHConfigParser() + with Path(self.user_config).open() as config: + parser.parse(config) + + parsed_hosts = [ + i for i in parser.get_hostnames() if self.is_endhost(i) + ] + + to_import = [] + + for alias in parsed_hosts: + parsed_host = parser.lookup(alias) + + if 'ignore' in parsed_host: + continue + + to_import.append( + self.adapter.adapt_ssh_config_host_to_instance( + alias, parsed_host + ) + ) + + return to_import + + def is_endhost(self, hostname): + """Return true when passed hostname is not wildcarded one.""" + return self.allowed_host_re.match(hostname) is None + + def export_host(self, export_file, alias, attributes): + """Write host to target file.""" + def make_param(param, value): + return '\n %s %s' % (param, value) + + host_string = '\nHost %s' % alias + + for key, value in attributes.iteritems(): + host_string += make_param(key, value) + + export_file.write(host_string + '\n') diff --git a/termius/sync/commands.py b/termius/sync/commands.py deleted file mode 100644 index fc9a03a..0000000 --- a/termius/sync/commands.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -"""Module with CLI command to retrieve SaaS and IaaS hosts.""" -from stevedore.extension import ExtensionManager -from ..core.commands import AbstractCommand -from ..core.storage.strategies import ( - RelatedSaveStrategy, RelatedGetStrategy -) - - -class SyncCommand(AbstractCommand): - """Sync with IaaS or PaaS.""" - - service_manager = ExtensionManager( - namespace='termius.sync.providers' - ) - save_strategy = RelatedSaveStrategy - get_strategy = RelatedGetStrategy - - def extend_parser(self, parser): - """Add more arguments to parser.""" - parser.add_argument( - '-c', '--credentials', - help='Credentials (path or file) for service.' - ) - parser.add_argument('service', metavar='SERVICE', help='Service name.') - return parser - - def get_service(self, service_name): - """Load service instance by name.""" - try: - extension = self.service_manager[service_name] - except KeyError: - raise NoSuchServiceException( - 'Do not support service: {}.'.format(service_name) - ) - return extension.plugin - - def sync_with_service(self, service, credentials): - """Connect to service and retrieve it's hosts..""" - service_class = self.get_service(service) - service = service_class(self.storage, credentials) - service.sync() - - def take_action(self, parsed_args): - """Process CLI call.""" - self.sync_with_service(parsed_args.service, - parsed_args.credentials) - self.log.info('Sync with service %s.', parsed_args.service) - - -class NoSuchServiceException(Exception): - """Raise it when service name are not found.""" diff --git a/termius/sync/providers/ssh.py b/termius/sync/providers/ssh.py deleted file mode 100644 index eb90662..0000000 --- a/termius/sync/providers/ssh.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -"""Module with ssh sync provider.""" -from os import environ -from os.path import expanduser -import re -from pathlib2 import Path -from paramiko.config import SSHConfig -from .base import BaseSyncService -from ...core.models.terminal import Host, SshConfig, Identity, SshKey - - -class SSHService(BaseSyncService): - """Synchronize ssh config content with application.""" - - user_config = '~/.ssh/config' - # pylint: disable=anomalous-backslash-in-string - allowed_host_re = re.compile('[?*\[\]]') - default_user = environ.get('USER', None) - default_port = 22 - - def __init__(self, *args, **kwargs): - """Contruct new service to sync ssh config.""" - super(SSHService, self).__init__(*args, **kwargs) - self.user_config = expanduser(self.user_config) - - def hosts(self): - """Retrieve host instances from ssh config.""" - config = SSHConfig() - with Path(self.user_config).open() as fileobj: - config.parse(fileobj) - hostnames = [i for i in config.get_hostnames() if self.is_endhost(i)] - return [self.transform_to_instances(i, config.lookup(i)) - for i in hostnames] - - def transform_to_instances(self, alias, host): - """Convert paramico host to application host.""" - app_host = Host( - label=alias, - address=host['hostname'], - ) - ssh_config = SshConfig( - identity=Identity( - username=host.get('user', self.default_user), - ssh_key=self.create_key(host) - ) - ) - - ssh_config.port = host.get('port') - ssh_config.timeout = host.get('serveraliveinterval') - ssh_config.keep_alive_packages = host.get('serveralivecountmax') - ssh_config.use_ssh_key = host.get('identitiesonly') - ssh_config.strict_host_key_check = host.get('stricthostkeychecking') - - app_host.ssh_config = ssh_config - return app_host - - def is_endhost(self, hostname): - """Return true when passed hostname is not wildcarded one.""" - return self.allowed_host_re.match(hostname) is 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] diff --git a/tests/integration/export-ssh-config.bats b/tests/integration/export-ssh-config.bats new file mode 100644 index 0000000..adb2276 --- /dev/null +++ b/tests/integration/export-ssh-config.bats @@ -0,0 +1,11 @@ +#!/usr/bin/env bats + +@test "export-ssh-config help by arg" { + run termius import-ssh-config --help + [ "$status" -eq 0 ] +} + +@test "export-ssh-config help command" { + run termius help import-ssh-config + [ "$status" -eq 0 ] +} diff --git a/tests/integration/import-ssh-config.bats b/tests/integration/import-ssh-config.bats new file mode 100644 index 0000000..8e1fcca --- /dev/null +++ b/tests/integration/import-ssh-config.bats @@ -0,0 +1,11 @@ +#!/usr/bin/env bats + +@test "import-ssh-config help by arg" { + run termius import-ssh-config --help + [ "$status" -eq 0 ] +} + +@test "import-ssh-config help command" { + run termius help import-ssh-config + [ "$status" -eq 0 ] +} diff --git a/tests/integration/sync.bats b/tests/integration/sync.bats deleted file mode 100644 index ef8124f..0000000 --- a/tests/integration/sync.bats +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bats - -@test "sync help by arg" { - run termius sync --help - [ "$status" -eq 0 ] -} - -@test "sync help command" { - run termius help sync - [ "$status" -eq 0 ] -} - -@test "sync not supported service" { - run termius sync awesomeservice - [ "$status" -eq 1 ] - [ "$output" = "Do not support service: awesomeservice." ] -} diff --git a/tests/unit/sync/__init__.py b/tests/unit/porting/__init__.py similarity index 100% rename from tests/unit/sync/__init__.py rename to tests/unit/porting/__init__.py diff --git a/tests/unit/sync/providers/__init__.py b/tests/unit/porting/providers/__init__.py similarity index 100% rename from tests/unit/sync/providers/__init__.py rename to tests/unit/porting/providers/__init__.py diff --git a/tests/unit/sync/providers/ssh_test.py b/tests/unit/porting/providers/ssh_test.py similarity index 77% rename from tests/unit/sync/providers/ssh_test.py rename to tests/unit/porting/providers/ssh_test.py index 09b75c2..e8e1a94 100644 --- a/tests/unit/sync/providers/ssh_test.py +++ b/tests/unit/porting/providers/ssh_test.py @@ -3,21 +3,21 @@ from mock import patch, MagicMock from six import StringIO from nose.tools import eq_ -from termius.sync.providers.ssh import SSHService +from termius.porting.providers.ssh.provider import SSHPortingProvider from termius.core.models.terminal import ( Host, SshConfig, Identity, SshKey ) -@patch('termius.sync.providers.ssh.Path') +@patch('termius.porting.providers.ssh.provider.Path') def test_empty_sshconfig(mockpath): mockpath.return_value.open.return_value.__enter__.return_value = StringIO() - service = SSHService(None, '') - ssh_hosts = service.hosts() + service = SSHPortingProvider(None, '') + ssh_hosts = service.provider_hosts() eq_(ssh_hosts, []) -@patch('termius.sync.providers.ssh.Path') +@patch('termius.porting.providers.ssh.provider.Path') def test_single_sshconfig(mockpath): mockpath.return_value.open.return_value.__enter__.return_value = StringIO( """ @@ -30,8 +30,8 @@ def test_single_sshconfig(mockpath): StrictHostKeyChecking no """ ) - service = SSHService(None, '') - ssh_hosts = service.hosts() + service = SSHPortingProvider(None, '') + ssh_hosts = service.provider_hosts() eq_(ssh_hosts, [ Host( label='firstone', @@ -51,7 +51,7 @@ def test_single_sshconfig(mockpath): ]) -@patch('termius.sync.providers.ssh.Path') +@patch('termius.porting.providers.ssh.provider.Path') def test_single_sshconfig_with_fnmatch(mockpath): mockpath.return_value.open.return_value.__enter__.return_value = StringIO( """ @@ -63,8 +63,8 @@ def test_single_sshconfig_with_fnmatch(mockpath): User ubuntu """ ) - service = SSHService(None, '') - ssh_hosts = service.hosts() + service = SSHPortingProvider(None, '') + ssh_hosts = service.provider_hosts() eq_(ssh_hosts, [ Host( label='firstone', @@ -84,8 +84,9 @@ def test_single_sshconfig_with_fnmatch(mockpath): ]) -@patch('termius.sync.providers.ssh.Path') -def test_single_sshconfig_with_keys(mockpath): +@patch('termius.porting.providers.ssh.provider.Path') +@patch('termius.porting.providers.ssh.adapter.Path') +def test_single_sshconfig_with_keys(path_in_providers, path_in_adapters): sshconfig_content = """ Host firstone HostName localhost @@ -99,10 +100,11 @@ def test_single_sshconfig_with_keys(mockpath): '~/.ssh/id_rsa': private_key_content }) - mockpath.side_effect = fake_files.generate_path_obj + path_in_providers.side_effect = fake_files.generate_path_obj + path_in_adapters.side_effect = fake_files.generate_path_obj - service = SSHService(None, '') - ssh_hosts = service.hosts() + service = SSHPortingProvider(None, '') + ssh_hosts = service.provider_hosts() eq_(ssh_hosts, [ Host( label='firstone', @@ -126,18 +128,18 @@ def test_single_sshconfig_with_keys(mockpath): class FakePathsObj(object): - def __init__(self, **kwargs): self.files = { os.path.expanduser(i): content for i, content in kwargs.items() - } + } def generate_path_obj(self, path): mock = MagicMock() mock.name = os.path.basename(path) path = os.path.expanduser(path) mock.is_file.return_value = path in self.files - mock.open.return_value.__enter__.side_effect = lambda: StringIO(self.files[path]) + mock.open.return_value.__enter__.side_effect = lambda: StringIO( + self.files[path]) mock.read_text.side_effect = lambda: self.files[path] return mock