From 46568ff30fbc2e768bbc78f21feb305bfada10d3 Mon Sep 17 00:00:00 2001 From: Maxbey Date: Thu, 10 Aug 2017 18:54:53 +0600 Subject: [PATCH] Implement hosts import from SecureCRT --- setup.py | 1 + termius/core/storage/__init__.py | 6 +- termius/porting/commands.py | 43 ++++++++++ .../porting/providers/securecrt/__init__.py | 2 + termius/porting/providers/securecrt/parser.py | 83 ++++++++++++++++++ .../porting/providers/securecrt/provider.py | 86 +++++++++++++++++++ tests/unit/core/storage/storage_test.py | 19 +++- 7 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 termius/porting/providers/securecrt/__init__.py create mode 100644 termius/porting/providers/securecrt/parser.py create mode 100644 termius/porting/providers/securecrt/provider.py diff --git a/setup.py b/setup.py index 2d6960b..7c4fa0a 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ handlers = [ 'import-ssh-config = termius.porting.commands:SSHImportCommand', 'export-ssh-config = termius.porting.commands:SSHExportCommand', + 'import-hosts = termius.porting.commands:ImportHostsCommand', 'login = termius.account.commands:LoginCommand', 'logout = termius.account.commands:LogoutCommand', 'settings = termius.account.commands:SettingsCommand', diff --git a/termius/core/storage/__init__.py b/termius/core/storage/__init__.py index a947671..b09406f 100644 --- a/termius/core/storage/__init__.py +++ b/termius/core/storage/__init__.py @@ -80,17 +80,19 @@ def __exit__(self, exc_type, exc_val, exc_tb): """Process transaction closing and sync driver.""" self.driver.sync() - def save(self, model): + def save(self, original_model): """Save model to storage. Will return model with id and saved mapped fields Model instances with ids. """ - model = self.strategies.saver.save(model) + model = self.strategies.saver.save(original_model) if getattr(model, model.id_name): saved_model = self.update(model) else: saved_model = self.create(model) + original_model.id = model.id + return saved_model def create(self, model): diff --git a/termius/porting/commands.py b/termius/porting/commands.py index b1ba414..d911bee 100644 --- a/termius/porting/commands.py +++ b/termius/porting/commands.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- """Module with CLI command to export and import hosts.""" +from termius.core.commands.single import RequiredOptions, DetailCommand from termius.core.storage.strategies import ( RelatedSaveStrategy, RelatedGetStrategy ) +from termius.porting.providers.securecrt.provider import \ + SecureCRTPortingProvider from termius.porting.providers.ssh.provider import SSHPortingProvider @@ -48,3 +51,43 @@ def take_action(self, parsed_args): self.log.info( 'Export hosts from local storage to ~/.termius/sshconfig' ) + + +class ImportHostsCommand(DetailCommand): + """import hosts from the specified provider""" + + required_options = RequiredOptions(create=('provider', 'source')) + providers = { + 'securecrt': SecureCRTPortingProvider + } + + def take_action(self, parsed_args): + """Process CLI call.""" + self.validate_args(parsed_args, 'create') + provider_name = parsed_args.provider.lower() + if provider_name not in self.providers: + self.log.error('Wrong provider name was specified!') + return + + provider = self.providers[provider_name]( + source=parsed_args.source, storage=self.storage, crendetial=None + ) + provider.import_hosts() + self.log.info('SecureCRT hosts has been successfully exported.') + + def get_parser(self, prog_name): + """Skip detail arguments.""" + return super(DetailCommand, self).get_parser(prog_name) + + def extend_parser(self, parser): + """Add more arguments to parser.""" + parser.add_argument( + '-p', '--provider', + metavar='PROVIDER', help='the name of provider (SecureCRT)' + ) + parser.add_argument( + '-s', '--source', + metavar='SOURCE', help='path to source file' + ) + + return parser diff --git a/termius/porting/providers/securecrt/__init__.py b/termius/porting/providers/securecrt/__init__.py new file mode 100644 index 0000000..d62d38a --- /dev/null +++ b/termius/porting/providers/securecrt/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Package with logic to import hosts from SecureCRT provider.""" diff --git a/termius/porting/providers/securecrt/parser.py b/termius/porting/providers/securecrt/parser.py new file mode 100644 index 0000000..decdf79 --- /dev/null +++ b/termius/porting/providers/securecrt/parser.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +"""Module with SecureCRT parser.""" +from os.path import expanduser + + +class SecureCRTConfigParser(object): + """SecureCRT xml parser.""" + + meta_sessions = ['Default'] + + @classmethod + def parse_hosts(cls, xml): + """Parse SecureCRT Sessions.""" + sessions = cls.get_element_by_name( + xml.getchildren(), 'Sessions' + ).getchildren() + + parsed_hosts = [] + + for session in sessions: + if session.get('name') not in cls.meta_sessions: + parsed_hosts.append(cls.make_host(session)) + + return parsed_hosts + + @classmethod + def parse_identity(cls, xml): + """Parse SecureCRT SSH2 raw key.""" + identity = cls.get_element_by_name( + xml.getchildren(), 'SSH2' + ) + if identity is None: + return None + + identity_filename = cls.get_element_by_name( + identity.getchildren(), + 'Identity Filename V2' + ) + + if identity_filename is None: + return None + + path = identity_filename.text.split('/') + public_key_name = path[-1].split('::')[0] + private_key_name = public_key_name.split('.')[0] + + if path[0].startswith('$'): + path.pop(0) + path.insert(0, expanduser("~")) + + path[-1] = public_key_name + public_key_path = '/'.join(path) + path[-1] = private_key_name + private_key_path = '/'.join(path) + + return private_key_path, public_key_path + + @classmethod + def make_host(cls, session): + """Adapt SecureCRT Session to Termius host.""" + session_attrs = session.getchildren() + + return { + 'label': session.get('name'), + 'hostname': cls.get_element_by_name( + session_attrs, 'Hostname' + ).text, + 'port': cls.get_element_by_name( + session_attrs, '[SSH2] Port' + ).text, + 'username': cls.get_element_by_name( + session_attrs, 'Username' + ).text + } + + @classmethod + def get_element_by_name(cls, elements, name): + """Get SecureCRT config block.""" + for element in elements: + if element.get('name') == name: + return element + + return None diff --git a/termius/porting/providers/securecrt/provider.py b/termius/porting/providers/securecrt/provider.py new file mode 100644 index 0000000..c696a42 --- /dev/null +++ b/termius/porting/providers/securecrt/provider.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +"""Module with SecureCRT provider.""" +import logging +import xml + +from termius.core.models.terminal import Host, SshConfig, Identity, SshKey, \ + Group +from termius.porting.providers.securecrt.parser import SecureCRTConfigParser + +from ..base import BasePortingProvider + + +class SecureCRTPortingProvider(BasePortingProvider): + """Synchronize secure crt config content with application.""" + + logger = logging.getLogger(__name__) + + def __init__(self, source, *args, **kwargs): + """Contruct new service to sync ssh config.""" + super(SecureCRTPortingProvider, self).__init__(*args, **kwargs) + + self.config_source = source + + def export_hosts(self): + """Skip export.""" + pass + + def provider_hosts(self): + """Retrieve host instances from ssh config.""" + root = xml.etree.ElementTree.parse(self.config_source).getroot() + hosts = [] + + raw_hosts = SecureCRTConfigParser.parse_hosts( + root + ) + identity_paths = SecureCRTConfigParser.parse_identity(root) + main_group = Group(label='SecureCRT') + + group_config = SshConfig( + identity=Identity( + is_visible=False, + label='SecureCRT' + ) + ) + + if identity_paths: + try: + with open(identity_paths[0], 'rb') as private_key_file: + private_key = private_key_file.read() + + with open(identity_paths[1], 'rb') as public_key_file: + public_key = public_key_file.read() + + key = SshKey( + label='SecureCRT', + private_key=private_key, + public_key=public_key + ) + group_config.identity.ssh_key = key + except IOError: + self.logger.info( + 'Cannot find SSH2 raw key %s' % identity_paths[1] + ) + + main_group.ssh_config = group_config + + for raw_host in raw_hosts: + host = Host( + label=raw_host['label'], + address=raw_host['hostname'] + ) + host.group = main_group + ssh_config = SshConfig( + port=raw_host['port'], + identity=Identity( + username=raw_host.get('username'), + is_visible=False, + label=raw_host.get('username') + ) + ) + + host.ssh_config = ssh_config + + hosts.append(host) + + return hosts diff --git a/tests/unit/core/storage/storage_test.py b/tests/unit/core/storage/storage_test.py index efaf1ac..b6e1f1d 100644 --- a/tests/unit/core/storage/storage_test.py +++ b/tests/unit/core/storage/storage_test.py @@ -200,7 +200,6 @@ def test_save_strategy(self): saved_group = self.storage.get(Group, id=saved_host.group) self.assertIsNotNone(saved_group.id) - saved_sshconfig = self.storage.get(SshConfig, id=saved_host.ssh_config) self.assertIsNotNone(saved_sshconfig.id) self.assertIsInstance(saved_sshconfig.identity, Identity) @@ -219,9 +218,27 @@ def test_save_strategy(self): self.assertIsNotNone(saved_sshkey.id) def test_save_2_times(self): + groups_count_before = len(self.storage.get_all(Group)) + keys_count_before = len(self.storage.get_all(SshKey)) + identities_count_before = len(self.storage.get_all(Identity)) + hosts_count_before = len(self.storage.get_all(Host)) + for _ in range(2): self.test_save_strategy() + self.assertEquals( + groups_count_before + 1, len(self.storage.get_all(Group)) + ) + self.assertEquals( + keys_count_before + 1, len(self.storage.get_all(SshKey)) + ) + self.assertEquals( + identities_count_before + 1, len(self.storage.get_all(Identity)) + ) + self.assertEquals( + hosts_count_before + 1, len(self.storage.get_all(Host)) + ) + def test_get_strategy(self): self.identity.ssh_key = self.sshkey self.sshconfig.identity = self.identity