Skip to content

Commit

Permalink
Implement hosts import from SecureCRT
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxbey committed Aug 11, 2017
1 parent bf55d42 commit 46568ff
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 3 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 4 additions & 2 deletions termius/core/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
43 changes: 43 additions & 0 deletions termius/porting/commands.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions termius/porting/providers/securecrt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""Package with logic to import hosts from SecureCRT provider."""
83 changes: 83 additions & 0 deletions termius/porting/providers/securecrt/parser.py
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions termius/porting/providers/securecrt/provider.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 18 additions & 1 deletion tests/unit/core/storage/storage_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit 46568ff

Please sign in to comment.