From 71e88584aa1004786407ea900f9dc9afa838b62c Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Tue, 26 Jan 2016 16:04:33 +0600 Subject: [PATCH 01/10] :fire:Some code style fixes, also clean up old code. Also add some disables to doc linter. --- .prospector.yaml | 11 ++ pavement.py | 4 +- serverauditor_sshconfig/account/commands.py | 5 +- serverauditor_sshconfig/account/managers.py | 5 - .../cloud/commands/host.py | 8 +- serverauditor_sshconfig/cloud/models.py | 5 +- serverauditor_sshconfig/cloud/serializers.py | 11 +- serverauditor_sshconfig/core/api.py | 4 - serverauditor_sshconfig/core/application.py | 187 ------------------ serverauditor_sshconfig/core/logger.py | 40 ---- serverauditor_sshconfig/core/models.py | 3 +- .../core/storage/driver.py | 6 +- .../core/storage/idgenerators.py | 5 +- .../core/storage/operators.py | 7 +- .../core/storage/strategies.py | 2 +- serverauditor_sshconfig/core/utils.py | 11 +- serverauditor_sshconfig/handlers.py | 1 - serverauditor_sshconfig/main.py | 5 - serverauditor_sshconfig/sync/commands.py | 1 - serverauditor_sshconfig/sync/services/base.py | 2 +- 20 files changed, 46 insertions(+), 277 deletions(-) delete mode 100644 serverauditor_sshconfig/core/application.py delete mode 100644 serverauditor_sshconfig/core/logger.py diff --git a/.prospector.yaml b/.prospector.yaml index 92d81be..112c723 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -5,6 +5,17 @@ inherits: ignore-paths: - .git + - setup.py + +pep257: + disable: + - D100 # TODO remove it when done with code style fixing. + - D101 # TODO remove it when done with code style fixing. + - D102 # TODO remove it when done with code style fixing. + - D103 # TODO remove it when done with code style fixing. + - D104 # TODO remove it when done with code style fixing. + - D105 # TODO remove it when done with code style fixing. + - D203 # 1 blank line required before pylint: options: diff --git a/pavement.py b/pavement.py index f1d4996..91f7be4 100644 --- a/pavement.py +++ b/pavement.py @@ -85,15 +85,17 @@ def get_version(): @task @needs('generate_setup', 'minilib', 'setuptools.command.sdist') def sdist(): - """Overrides sdist to make sure that our setup.py is generated.""" + """Override sdist to make sure that our setup.py is generated.""" pass @task def lint(): + """Check code style and conventions.""" sh('prospector') @task def bats(): + """Run tests on CLI usage.""" sh('bats tests/integration') diff --git a/serverauditor_sshconfig/account/commands.py b/serverauditor_sshconfig/account/commands.py index 09773ab..b2bc90d 100644 --- a/serverauditor_sshconfig/account/commands.py +++ b/serverauditor_sshconfig/account/commands.py @@ -1,8 +1,5 @@ # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" + import six from getpass import getpass from ..core.commands import AbstractCommand diff --git a/serverauditor_sshconfig/account/managers.py b/serverauditor_sshconfig/account/managers.py index 9ae5d97..bafc018 100644 --- a/serverauditor_sshconfig/account/managers.py +++ b/serverauditor_sshconfig/account/managers.py @@ -1,10 +1,5 @@ # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - from ..core.api import API diff --git a/serverauditor_sshconfig/cloud/commands/host.py b/serverauditor_sshconfig/cloud/commands/host.py index ee03cd2..e67d66e 100644 --- a/serverauditor_sshconfig/cloud/commands/host.py +++ b/serverauditor_sshconfig/cloud/commands/host.py @@ -6,7 +6,6 @@ class HostCommand(DetailCommand): - """Operate with Host object.""" allowed_operations = DetailCommand.all_operations @@ -47,11 +46,13 @@ def create(self, parsed_args): def serialize_args(self, args, instance=None): if instance: - ssh_identity = (instance.ssh_config and instance.ssh_config.ssh_identity) or SshIdentity() + ssh_identity = ( + instance.ssh_config and instance.ssh_config.ssh_identity + ) or SshIdentity() ssh_config = instance.ssh_config or SshConfig() host = instance else: - host, ssh_config, ssh_identity = Host(), SshConfig(), SshIdentity() + host, ssh_config, ssh_identity = Host(), SshConfig(), SshIdentity() if args.generate_key: raise NotImplementedError('Not implemented') @@ -74,7 +75,6 @@ def serialize_args(self, args, instance=None): class HostsCommand(ListCommand): - """Manage host objects.""" def get_parser(self, prog_name): diff --git a/serverauditor_sshconfig/cloud/models.py b/serverauditor_sshconfig/cloud/models.py index 67f4715..133c7ee 100644 --- a/serverauditor_sshconfig/cloud/models.py +++ b/serverauditor_sshconfig/cloud/models.py @@ -1,4 +1,4 @@ -from ..core.models import AbstractModel, Model, Field +from ..core.models import Model, Field class Tag(Model): @@ -38,7 +38,6 @@ class SshIdentity(Model): 'label': Field(str, False, ''), 'username': Field(str, False, ''), 'password': Field(str, False, ''), - 'ssh_key': Field(str, False, None), 'is_visible': Field(str, False, False), 'ssh_key': Field(SshKey, False, None), } @@ -63,7 +62,7 @@ class Group(Model): 'ssh_config': Field(SshConfig, False, None), } set_name = 'group_set' - crypto_fields = {'label',} + crypto_fields = {'label', } Group.fields['parent_group'] = Field(Group, False, None) diff --git a/serverauditor_sshconfig/cloud/serializers.py b/serverauditor_sshconfig/cloud/serializers.py index e72814e..1d2acec 100644 --- a/serverauditor_sshconfig/cloud/serializers.py +++ b/serverauditor_sshconfig/cloud/serializers.py @@ -16,6 +16,7 @@ ID_GETTER = itemgetter('id') + def map_zip_model_fields(model, field_getter=None): field_getter = field_getter or attrgetter(model.fields) return zip(model.fields, field_getter(model)) @@ -48,7 +49,7 @@ def __init__(self, model_class, **kwargs): class BulkPrimaryKeySerializer(BulkEntryBaseSerializer): to_model_mapping = defaultdict( - lambda: ID_GETTER, {int: int,} + lambda: ID_GETTER, {int: int, } ) def id_from_payload(self, payload): @@ -82,8 +83,8 @@ def get_primary_key_serializer(self, model_class): ) - -class BulkEntrySerializer(GetPrimaryKeySerializerMixin, BulkPrimaryKeySerializer): +class BulkEntrySerializer(GetPrimaryKeySerializerMixin, + BulkPrimaryKeySerializer): def __init__(self, **kwargs): super(BulkEntrySerializer, self).__init__(**kwargs) @@ -98,7 +99,9 @@ def to_payload(self, model): ) payload.update(zipped_remote_instance) for field, mapping in model.fields.items(): - payload[field] = self.serialize_related_field(model, field, mapping) + payload[field] = self.serialize_related_field( + model, field, mapping + ) payload['local_id'] = model.id return payload diff --git a/serverauditor_sshconfig/core/api.py b/serverauditor_sshconfig/core/api.py index dbe2ce4..636e2af 100644 --- a/serverauditor_sshconfig/core/api.py +++ b/serverauditor_sshconfig/core/api.py @@ -1,9 +1,5 @@ # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" import logging import six import hashlib diff --git a/serverauditor_sshconfig/core/application.py b/serverauditor_sshconfig/core/application.py deleted file mode 100644 index 7133f56..0000000 --- a/serverauditor_sshconfig/core/application.py +++ /dev/null @@ -1,187 +0,0 @@ -# coding: utf-8 - -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - -import base64 -try: - import configparser as ConfigParser -except ImportError: - import ConfigParser -import functools -import getpass -import hashlib -import os -import sys -import traceback - -from .utils import p_input, p_map, to_bytes -from .cryptor import CryptorException - -def description(greeting=None, valediction="Success!"): - - def decorator(func): - - @functools.wraps(func) - def wrapped(self): - if greeting: - self._logger.log(greeting) - try: - func(self) - except CryptorException as e: - if e.args[0] == 'Bad data!': - raise - else: - self._logger.log("Error! %s\n%s" % (e, traceback.format_exc()), file=sys.stderr, color='red') - sys.exit(1) - except Exception as exc: - self._logger.log("Error! %s\n%s" % (exc, traceback.format_exc()), file=sys.stderr, color='red') - sys.exit(1) - - self._logger.log(valediction, color='green') - return - - return wrapped - - return decorator - - -class SSHConfigApplication(object): - - SERVER_AUDITOR_SETTINGS_PATH = os.path.expanduser('~/.serverauditor') - - def __init__(self, api, ssh_config, cryptor, logger): - self._api = api - self._config = ssh_config - self._cryptor = cryptor - self._logger = logger - - self._sa_username = '' - self._sa_master_password = '' - self._sa_auth_key = '' - - self._sa_keys = {} - self._sa_connections = [] - - self._local_hosts = [] - self._full_local_hosts = [] - return - - def run(self): - pass - - def _valediction(self): - self._logger.log("Bye!", color='magenta') - return - - @description() - def _get_sa_user(self): - def hash_password(password): - password = to_bytes(password) - return hashlib.sha256(password).hexdigest() - - def read_name_from_config(): - settings_path = self.SERVER_AUDITOR_SETTINGS_PATH - if not os.path.exists(settings_path): - with open(settings_path, 'w+'): - pass - return None - settings = ConfigParser.ConfigParser() - settings.read([settings_path]) - try: - return settings.get('User', 'name') - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - return None - - def write_name_to_config(name): - settings = ConfigParser.ConfigParser() - settings.add_section('User') - settings.set('User', 'name', name) - with open(self.SERVER_AUDITOR_SETTINGS_PATH, 'w') as f: - settings.write(f) - return - - prompt = "Enter your Server Auditor's username%s: " - name = read_name_from_config() - if name: - prompt %= (' [%s]' % name) - self._sa_username = p_input(prompt).strip() or name - else: - prompt %= '' - self._sa_username = p_input(prompt).strip() - - write_name_to_config(self._sa_username) - self._sa_master_password = getpass.getpass("Enter your Server Auditor's password: ") - data = self._api.get_auth_key(self._sa_username, hash_password(self._sa_master_password)) - self._sa_auth_key = data['key'] - self._cryptor.password = self._sa_master_password - self._cryptor.encryption_salt = base64.decodestring(to_bytes(data['salt'])) - self._cryptor.hmac_salt = base64.decodestring(to_bytes(data['hmac_salt'])) - return - - @description("Getting current keys and connections...") - def _get_sa_keys_and_connections(self): - keys, self._sa_connections = self._api.get_keys_and_connections(self._sa_username, self._sa_auth_key) - self._sa_keys = {} - for key in keys: - self._sa_keys[key['id']] = key - return - - @description("Decrypting keys and connections...") - def _decrypt_sa_keys_and_connections(self): - def decrypt_key(kv): - key = kv[0] - v = kv[1] - try: - value = { - 'label': self._cryptor.decrypt(v['label']), - 'private_key': v['private_key'] and self._cryptor.decrypt(v['private_key']), - 'public_key': v['public_key'] and self._cryptor.decrypt(v['public_key']), - } - except CryptorException as e: - return key, None - return key, value - - def decrypt_connection(con): - try: - con['label'] = con['label'] and self._cryptor.decrypt(con['label']) - con['hostname'] = self._cryptor.decrypt(con['hostname']) - con['ssh_username'] = self._cryptor.decrypt(con['ssh_username']) - except CryptorException as e: - return None - return con - - self._sa_keys = dict(p_map(decrypt_key, list(self._sa_keys.items()))) - self._sa_connections = p_map(decrypt_connection, self._sa_connections) - - return - - @description("Fixing keys and connections...") - def _fix_sa_keys_and_connections(self): - self._sa_connections = [c for c in self._sa_connections if c is not None] - - def remove_key(key): - for i in self._sa_connections: - if i['ssh_key'] == key: - i['ssh_key'] = None - self._sa_keys.pop(key) - - for key in self._sa_keys.keys(): - if self._sa_keys[key] is None: - remove_key(key) - return - - - @description("Parsing ssh config file...") - def _parse_local_config(self): - self._config.parse() - self._local_hosts = self._config.get_complete_hosts() - return - - def _get_sa_connection_uri(self, conn): - return '{conn[ssh_username]}@{conn[hostname]}:{conn[port]}'.format(conn=conn) - - def _get_sa_connection_name(self, conn): - return conn['label'] or self._get_sa_connection_uri(conn) diff --git a/serverauditor_sshconfig/core/logger.py b/serverauditor_sshconfig/core/logger.py deleted file mode 100644 index a8bbdaa..0000000 --- a/serverauditor_sshconfig/core/logger.py +++ /dev/null @@ -1,40 +0,0 @@ -# coding: utf-8 - -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - -from __future__ import print_function - -import pprint -import sys -import time - - -class PrettyLogger(object): - - COLOR_END = '\033[0m' - COLOR_BOLD = '\033[1m' - COLORS = { - 'red': '\033[91m', - 'green': '\033[92m', - 'blue': '\033[94m', - 'yellow': '\033[93m', - 'magenta': '\033[95m', - 'end': COLOR_END - } - - def log(self, message, sleep=0.5, color='end', color_bold=False, is_pprint=False, *args, **kwargs): - fl = kwargs.get('file', sys.stdout) - print(self.COLORS.get(color, self.COLOR_END), end='', file=fl) - if color_bold: - print(self.COLOR_BOLD, end='', file=fl) - if is_pprint: - pprint.pprint(message, *args, **kwargs) - else: - print(message, end='', *args, **kwargs) - print(self.COLOR_END, file=fl) - if sleep: - time.sleep(sleep) - return diff --git a/serverauditor_sshconfig/core/models.py b/serverauditor_sshconfig/core/models.py index e5e399a..669cc27 100644 --- a/serverauditor_sshconfig/core/models.py +++ b/serverauditor_sshconfig/core/models.py @@ -55,7 +55,8 @@ class RemoteInstance(AbstractModel): fields = { 'id': Field(long, False, None), - 'state': Field(str, False, 'created'), # 'created' / 'updated' / 'synced' + # States could be one of 'created' / 'updated' / 'synced' + 'state': Field(str, False, 'created'), 'updated_at': Field(str, False, None), } diff --git a/serverauditor_sshconfig/core/storage/driver.py b/serverauditor_sshconfig/core/storage/driver.py index b45abe2..70081b8 100644 --- a/serverauditor_sshconfig/core/storage/driver.py +++ b/serverauditor_sshconfig/core/storage/driver.py @@ -61,7 +61,7 @@ def load(self, stream): class PersistentDict(OrderedDict): - '''Persistent dictionary with an API compatible with shelve and anydbm. + """Persistent dictionary with an API compatible with shelve and anydbm. The dict is kept in memory, so the dictionary operations run as fast as a regular dictionary. @@ -72,7 +72,7 @@ class PersistentDict(OrderedDict): Output file format is selectable between pickle, json, and csv. All three serialization formats are backed by fast C implementations. - ''' + """ def __init__(self, filename, flag='c', mode=None, format='json', *args, **kwds): @@ -92,7 +92,7 @@ def _read_mode(self): return 'rb' if format == 'pickle' else 'r' def sync(self): - 'Write dict to disk' + """Write dict to disk.""" def write_temp_file(filename): tempname = filename + '.tmp' diff --git a/serverauditor_sshconfig/core/storage/idgenerators.py b/serverauditor_sshconfig/core/storage/idgenerators.py index 563f850..c6833b1 100644 --- a/serverauditor_sshconfig/core/storage/idgenerators.py +++ b/serverauditor_sshconfig/core/storage/idgenerators.py @@ -10,7 +10,10 @@ def __init__(self, storage): """ def __call__(self, model): - """:param core.models.Model model: generate and set id for this Model.""" + """Generate id. + + :param core.models.Model model: generate and set id for this Model. + """ assert not getattr(model, model.id_name) identificator = uuid4().time_low setattr(model, model.id_name, identificator) diff --git a/serverauditor_sshconfig/core/storage/operators.py b/serverauditor_sshconfig/core/storage/operators.py index c0d5deb..ff362bf 100644 --- a/serverauditor_sshconfig/core/storage/operators.py +++ b/serverauditor_sshconfig/core/storage/operators.py @@ -1,4 +1,9 @@ -from operator import * + +# pylint: disable=unused-import +from operator import ( + eq, ne, gt, lt, le, ge, contains +) + def rcontains(obj, seq): return contains(seq, obj) diff --git a/serverauditor_sshconfig/core/storage/strategies.py b/serverauditor_sshconfig/core/storage/strategies.py index 0e7f65f..29bf4c6 100644 --- a/serverauditor_sshconfig/core/storage/strategies.py +++ b/serverauditor_sshconfig/core/storage/strategies.py @@ -1,5 +1,5 @@ import six -from ..models import DeleteSets, Model +from ..models import DeleteSets class Strategy(object): diff --git a/serverauditor_sshconfig/core/utils.py b/serverauditor_sshconfig/core/utils.py index 70edef5..a1d2cd2 100644 --- a/serverauditor_sshconfig/core/utils.py +++ b/serverauditor_sshconfig/core/utils.py @@ -5,11 +5,8 @@ License BSD, see LICENSE for more details. """ -import sys import os - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 +from six import PY2, PY3 if PY2: @@ -55,9 +52,3 @@ def bord(s): def expand_and_format_path(paths, **kwargs): return [os.path.expanduser(i.format(**kwargs)) for i in paths] - - -def resolve_attr(obj, attr): - for name in attr.split("."): - obj = getattr(obj, name) - return obj diff --git a/serverauditor_sshconfig/handlers.py b/serverauditor_sshconfig/handlers.py index 3b56974..fa62565 100644 --- a/serverauditor_sshconfig/handlers.py +++ b/serverauditor_sshconfig/handlers.py @@ -3,7 +3,6 @@ class ConnectCommand(AbstractCommand): - """Connect to specific host.""" def get_parser(self, prog_name): diff --git a/serverauditor_sshconfig/main.py b/serverauditor_sshconfig/main.py index cc399e9..94a6a64 100644 --- a/serverauditor_sshconfig/main.py +++ b/serverauditor_sshconfig/main.py @@ -1,11 +1,6 @@ #!/usr/bin/env python # coding: utf-8 -""" -Copyright (c) 2015 Crystalnix. -License BSD, see LICENSE for more details. -""" - import sys from serverauditor_sshconfig.app import ServerauditorApp diff --git a/serverauditor_sshconfig/sync/commands.py b/serverauditor_sshconfig/sync/commands.py index f63bc81..83c8959 100644 --- a/serverauditor_sshconfig/sync/commands.py +++ b/serverauditor_sshconfig/sync/commands.py @@ -8,7 +8,6 @@ class NoSuchServiceException(Exception): class SyncCommand(AbstractCommand): - """Sync with IaaS or PaaS.""" service_manager = ExtensionManager( diff --git a/serverauditor_sshconfig/sync/services/base.py b/serverauditor_sshconfig/sync/services/base.py index 2c16adb..9826a9b 100644 --- a/serverauditor_sshconfig/sync/services/base.py +++ b/serverauditor_sshconfig/sync/services/base.py @@ -1,5 +1,5 @@ -import six import abc +import six @six.add_metaclass(abc.ABCMeta) From 5a25b2f02e961f3aa39db8eb9204aee185af547f Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Tue, 26 Jan 2016 23:32:16 +0600 Subject: [PATCH 02/10] Fix inheritance issues and invalid naming. --- .prospector.yaml | 1 + pavement.py | 8 ++-- serverauditor_sshconfig/__init__.py | 4 ++ serverauditor_sshconfig/account/commands.py | 5 +- serverauditor_sshconfig/app.py | 10 ++-- .../cloud/commands/__init__.py | 15 +++--- .../cloud/commands/group.py | 13 +++-- .../cloud/commands/pf_rule.py | 6 +-- .../cloud/commands/snippet.py | 2 - .../cloud/commands/ssh_identity.py | 2 - .../cloud/commands/sync.py | 2 - serverauditor_sshconfig/cloud/commands/tag.py | 1 - serverauditor_sshconfig/cloud/serializers.py | 4 ++ serverauditor_sshconfig/core/api.py | 2 +- serverauditor_sshconfig/core/commands.py | 7 +-- serverauditor_sshconfig/core/settings.py | 9 +--- .../core/storage/driver.py | 15 +++--- .../core/storage/operators.py | 5 +- .../core/storage/strategies.py | 9 ++-- serverauditor_sshconfig/core/utils.py | 47 ------------------- serverauditor_sshconfig/sync/services/base.py | 7 ++- 21 files changed, 58 insertions(+), 116 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index 112c723..4644378 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -6,6 +6,7 @@ inherits: ignore-paths: - .git - setup.py + - build pep257: disable: diff --git a/pavement.py b/pavement.py index 91f7be4..c892874 100644 --- a/pavement.py +++ b/pavement.py @@ -6,12 +6,9 @@ sys.path.append('.') +from serverauditor_sshconfig import get_version -def get_version(): - from serverauditor_sshconfig import __version__ - return '.'.join(map(str, __version__)) - - +# pylint: disable=invalid-name requires = [ 'cliff==1.15', 'stevedore==1.10.0', @@ -23,6 +20,7 @@ def get_version(): 'pyasn1', ] +# pylint: disable=invalid-name handlers = [ 'sync = serverauditor_sshconfig.sync.commands:SyncCommand', 'snippet = serverauditor_sshconfig.cloud.commands:SnippetCommand', diff --git a/serverauditor_sshconfig/__init__.py b/serverauditor_sshconfig/__init__.py index 83aca41..a705a65 100644 --- a/serverauditor_sshconfig/__init__.py +++ b/serverauditor_sshconfig/__init__.py @@ -1 +1,5 @@ __version__ = (0, 7, 2) + + +def get_version(): + return '.'.join([str(i) for i in __version__]) diff --git a/serverauditor_sshconfig/account/commands.py b/serverauditor_sshconfig/account/commands.py index b2bc90d..d5fb292 100644 --- a/serverauditor_sshconfig/account/commands.py +++ b/serverauditor_sshconfig/account/commands.py @@ -1,7 +1,6 @@ # coding: utf-8 import six -from getpass import getpass from ..core.commands import AbstractCommand from .managers import AccountManager @@ -14,7 +13,6 @@ def __init__(self, app, app_args, cmd_name=None): class LoginCommand(BaseAccountCommand): - """Sign into serverauditor cloud.""" def prompt_username(self): @@ -35,7 +33,6 @@ def take_action(self, parsed_args): class LogoutCommand(BaseAccountCommand): - """Sign out serverauditor cloud.""" def get_parser(self, prog_name): @@ -43,6 +40,6 @@ def get_parser(self, prog_name): parser.add_argument('--clear-sshconfig', action='store_true') return parser - def take_action(self, parsed_args): + def take_action(self, _): self.manager.logout() self.log.info('Sign out serverauditor cloud.') diff --git a/serverauditor_sshconfig/app.py b/serverauditor_sshconfig/app.py index 4509ee9..64bf9c4 100644 --- a/serverauditor_sshconfig/app.py +++ b/serverauditor_sshconfig/app.py @@ -1,16 +1,14 @@ # coding: utf-8 import logging +# pylint: disable=import-error from cliff.app import App +# pylint: disable=import-error from cliff.commandmanager import CommandManager -from . import __version__ +from . import get_version -def get_version(): - return '.'.join(map(str, __version__)) - - -class ServerauditorApp(App): +class ServerauditorApp(object, App): def __init__(self): super(ServerauditorApp, self).__init__( diff --git a/serverauditor_sshconfig/cloud/commands/__init__.py b/serverauditor_sshconfig/cloud/commands/__init__.py index d5976f5..d2887bd 100644 --- a/serverauditor_sshconfig/cloud/commands/__init__.py +++ b/serverauditor_sshconfig/cloud/commands/__init__.py @@ -1,16 +1,15 @@ from ...core.commands import AbstractCommand -from .host import HostCommand, HostsCommand -from .group import GroupCommand, GroupsCommand -from .snippet import SnippetCommand, SnippetsCommand -from .pf_rule import PFRuleCommand, PFRulesCommand -from .ssh_identity import SshIdentityCommand, SshIdentitiesCommand -from .tag import TagsCommand -from .sync import PushCommand, PullCommand +from .host import HostCommand, HostsCommand # noqa +from .group import GroupCommand, GroupsCommand # noqa +from .snippet import SnippetCommand, SnippetsCommand # noqa +from .pf_rule import PFRuleCommand, PFRulesCommand # noqa +from .ssh_identity import SshIdentityCommand, SshIdentitiesCommand # noqa +from .tag import TagsCommand # noqa +from .sync import PushCommand, PullCommand # noqa class InfoCommand(AbstractCommand): - """Show info about host or group.""" def get_parser(self, prog_name): diff --git a/serverauditor_sshconfig/cloud/commands/group.py b/serverauditor_sshconfig/cloud/commands/group.py index 8b79bc2..25408ec 100644 --- a/serverauditor_sshconfig/cloud/commands/group.py +++ b/serverauditor_sshconfig/cloud/commands/group.py @@ -1,14 +1,10 @@ from operator import attrgetter -from ...core.exceptions import ( - InvalidArgumentException, TooManyEntriesException, DoesNotExistException, -) from ...core.commands import DetailCommand, ListCommand from ..models import Group, SshConfig, SshIdentity from .ssh_config import SshConfigArgs class GroupCommand(DetailCommand): - """Operate with Group object.""" allowed_operations = DetailCommand.all_operations @@ -37,11 +33,15 @@ def create(self, parsed_args): def serialize_args(self, args, instance=None): if instance: - ssh_identity = (instance.ssh_config and instance.ssh_config.ssh_identity) or SshIdentity() + ssh_identity = ( + instance.ssh_config and instance.ssh_config.ssh_identity + ) or SshIdentity() ssh_config = instance.ssh_config or SshConfig() group = instance else: - group, ssh_config, ssh_identity = Group(), SshConfig(), SshIdentity() + group, ssh_config, ssh_identity = ( + Group(), SshConfig(), SshIdentity() + ) if args.generate_key: raise NotImplementedError('Not implemented') @@ -62,7 +62,6 @@ def serialize_args(self, args, instance=None): class GroupsCommand(ListCommand): - """Manage group objects.""" def get_parser(self, prog_name): diff --git a/serverauditor_sshconfig/cloud/commands/pf_rule.py b/serverauditor_sshconfig/cloud/commands/pf_rule.py index c24d380..15e0a9a 100644 --- a/serverauditor_sshconfig/cloud/commands/pf_rule.py +++ b/serverauditor_sshconfig/cloud/commands/pf_rule.py @@ -9,7 +9,6 @@ class PFRuleCommand(DetailCommand): - """Operate with port forwarding rule object.""" allowed_operations = DetailCommand.all_operations @@ -73,8 +72,8 @@ def serialize_args(self, args, instance=None): pfrule.host = host if args.binding: binding_dict = self.parse_binding(pfrule.pf_type, args.binding) - for k, v in binding_dict.items(): - setattr(pfrule, k, v) + for key, value in binding_dict.items(): + setattr(pfrule, key, value) return pfrule def get_host(self, arg): @@ -92,7 +91,6 @@ def get_host(self, arg): class PFRulesCommand(ListCommand): - """Manage port forwarding rule objects.""" def take_action(self, parsed_args): diff --git a/serverauditor_sshconfig/cloud/commands/snippet.py b/serverauditor_sshconfig/cloud/commands/snippet.py index 1dec4cf..4120142 100644 --- a/serverauditor_sshconfig/cloud/commands/snippet.py +++ b/serverauditor_sshconfig/cloud/commands/snippet.py @@ -5,7 +5,6 @@ class SnippetCommand(DetailCommand): - """Operate with Group object.""" allowed_operations = DetailCommand.all_operations @@ -41,7 +40,6 @@ def serialize_args(self, args, instance=None): class SnippetsCommand(ListCommand): - """Manage snippet objects.""" def take_action(self, parsed_args): diff --git a/serverauditor_sshconfig/cloud/commands/ssh_identity.py b/serverauditor_sshconfig/cloud/commands/ssh_identity.py index 1e3b642..6f61386 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_identity.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_identity.py @@ -4,7 +4,6 @@ class SshIdentityCommand(DetailCommand): - """Operate with ssh identity object.""" allowed_operations = DetailCommand.all_operations @@ -62,7 +61,6 @@ def serialize_args(self, args, instance=None): class SshIdentitiesCommand(ListCommand): - """Manage ssh identity objects.""" def take_action(self, parsed_args): diff --git a/serverauditor_sshconfig/cloud/commands/sync.py b/serverauditor_sshconfig/cloud/commands/sync.py index c664f3d..f3eb78e 100644 --- a/serverauditor_sshconfig/cloud/commands/sync.py +++ b/serverauditor_sshconfig/cloud/commands/sync.py @@ -39,7 +39,6 @@ def take_action(self, parsed_args): class PushCommand(CloudSynchronizationCommand): - """Push data to Serverauditor cloud.""" get_strategy = RelatedGetStrategy @@ -50,7 +49,6 @@ def process_sync(self, api_controller): class PullCommand(CloudSynchronizationCommand): - """Pull data from Serverauditor cloud.""" def process_sync(self, api_controller): diff --git a/serverauditor_sshconfig/cloud/commands/tag.py b/serverauditor_sshconfig/cloud/commands/tag.py index a23bbfc..bd7b6a6 100644 --- a/serverauditor_sshconfig/cloud/commands/tag.py +++ b/serverauditor_sshconfig/cloud/commands/tag.py @@ -2,7 +2,6 @@ class TagsCommand(ListCommand): - """Manage tag objects.""" def get_parser(self, prog_name): diff --git a/serverauditor_sshconfig/cloud/serializers.py b/serverauditor_sshconfig/cloud/serializers.py index 1d2acec..501a52c 100644 --- a/serverauditor_sshconfig/cloud/serializers.py +++ b/serverauditor_sshconfig/cloud/serializers.py @@ -36,8 +36,10 @@ def to_model(self, payload): @abc.abstractmethod def to_payload(self, model): """Convert Application models to REST API payload.""" + pass +# pylint: disable=abstract-method class BulkEntryBaseSerializer(Serializer): def __init__(self, model_class, **kwargs): @@ -75,6 +77,7 @@ def to_payload(self, model): return '{model.set_name}/{model.id}'.format(model=model) +# pylint: disable=too-few-public-methods class GetPrimaryKeySerializerMixin(object): def get_primary_key_serializer(self, model_class): @@ -148,6 +151,7 @@ def initialize_model(self, payload): ) return model + # pylint: disable=no-self-use def create_remote_instance(self, payload): remote_instance = RemoteInstance() for i, field in RemoteInstance.fields.items(): diff --git a/serverauditor_sshconfig/core/api.py b/serverauditor_sshconfig/core/api.py index 636e2af..34378ea 100644 --- a/serverauditor_sshconfig/core/api.py +++ b/serverauditor_sshconfig/core/api.py @@ -58,7 +58,7 @@ def request_url(self, endpoint): return self.base_url + endpoint def login(self, username, password): - """Returns user's auth token.""" + """Return user's auth token.""" password = hash_password(password) response = requests.get(self.request_url("v1/token/auth/"), auth=(username, password)) diff --git a/serverauditor_sshconfig/core/commands.py b/serverauditor_sshconfig/core/commands.py index 3771313..9bca2af 100644 --- a/serverauditor_sshconfig/core/commands.py +++ b/serverauditor_sshconfig/core/commands.py @@ -2,7 +2,9 @@ import logging import getpass +# pylint: disable=import-error from cliff.command import Command +# pylint: disable=import-error from cliff.lister import Lister from .exceptions import DoesNotExistException, ArgumentRequiredException from .settings import Config @@ -22,8 +24,7 @@ def prompt_password(self): return getpass.getpass("Serverauditor's password:") -class AbstractCommand(PasswordPromptMixin, Command): - +class AbstractCommand(object, PasswordPromptMixin, Command): """Abstract Command with log.""" log = logging.getLogger(__name__) @@ -174,7 +175,7 @@ def delete_instance(self, instance): self.log_delete(instance) -class ListCommand(Lister): +class ListCommand(object, Lister): log = logging.getLogger(__name__) diff --git a/serverauditor_sshconfig/core/settings.py b/serverauditor_sshconfig/core/settings.py index b5bf014..19bf23c 100644 --- a/serverauditor_sshconfig/core/settings.py +++ b/serverauditor_sshconfig/core/settings.py @@ -1,10 +1,5 @@ # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - import os import six from .utils import expand_and_format_path @@ -50,5 +45,5 @@ def remove_section(self, section): self.config.remove_section(section) def write(self): - with open(self.user_config_path, 'w') as f: - self.config.write(f) + with open(self.user_config_path, 'w') as _file: + self.config.write(_file) diff --git a/serverauditor_sshconfig/core/storage/driver.py b/serverauditor_sshconfig/core/storage/driver.py index 70081b8..e3675cb 100644 --- a/serverauditor_sshconfig/core/storage/driver.py +++ b/serverauditor_sshconfig/core/storage/driver.py @@ -60,7 +60,6 @@ def load(self, stream): class PersistentDict(OrderedDict): - """Persistent dictionary with an API compatible with shelve and anydbm. The dict is kept in memory, so the dictionary operations run as fast as @@ -71,14 +70,13 @@ class PersistentDict(OrderedDict): Input file format is automatically discovered. Output file format is selectable between pickle, json, and csv. All three serialization formats are backed by fast C implementations. - """ - def __init__(self, filename, flag='c', mode=None, format='json', + def __init__(self, filename, flag='c', mode=None, _format='json', *args, **kwds): self.flag = flag # r=readonly, c=create, or n=new self.mode = mode # None or an octal triple like 0644 - self.format = format # 'csv', 'json', or 'pickle' + self._format = _format # 'csv', 'json', or 'pickle' self.filename = filename super(PersistentDict, self).__init__(*args, **kwds) if flag != 'n' and os.access(filename, os.R_OK): @@ -86,14 +84,13 @@ def __init__(self, filename, flag='c', mode=None, format='json', self.load(fileobj) def _write_mode(self): - return 'wb' if self.format == 'pickle' else 'w' + return 'wb' if self._format == 'pickle' else 'w' def _read_mode(self): - return 'rb' if format == 'pickle' else 'r' + return 'rb' if self._format == 'pickle' else 'r' def sync(self): """Write dict to disk.""" - def write_temp_file(filename): tempname = filename + '.tmp' try: @@ -124,9 +121,9 @@ def __exit__(self, *exc_info): def dump(self, fileobj): try: - DRIVERS[self.format].dump(fileobj, self) + DRIVERS[self._format].dump(fileobj, self) except KeyError: - raise NotImplementedError('Unknown format: ' + repr(self.format)) + raise NotImplementedError('Unknown format: ' + repr(self._format)) def load(self, fileobj): for loader in DRIVERS.values(): diff --git a/serverauditor_sshconfig/core/storage/operators.py b/serverauditor_sshconfig/core/storage/operators.py index ff362bf..8fc6e98 100644 --- a/serverauditor_sshconfig/core/storage/operators.py +++ b/serverauditor_sshconfig/core/storage/operators.py @@ -1,8 +1,5 @@ -# pylint: disable=unused-import -from operator import ( - eq, ne, gt, lt, le, ge, contains -) +from operator import * # noqa def rcontains(obj, seq): diff --git a/serverauditor_sshconfig/core/storage/strategies.py b/serverauditor_sshconfig/core/storage/strategies.py index 29bf4c6..83b37cb 100644 --- a/serverauditor_sshconfig/core/storage/strategies.py +++ b/serverauditor_sshconfig/core/storage/strategies.py @@ -2,6 +2,7 @@ from ..models import DeleteSets +# pylint: disable=too-few-public-methods class Strategy(object): def __init__(self, storage): @@ -37,6 +38,7 @@ def save_submodel(self, submodel, mapping): class GetStrategy(Strategy): + # pylint: disable=no-self-use def get(self, model): return model @@ -60,6 +62,7 @@ class DeleteStrategy(Strategy): def get_delete_sets(self): return {} + # pylint: disable=no-self-use def delete(self, model): return model @@ -91,7 +94,7 @@ def delete(self, model): def confirm_delete(self, sets): # FIXME It needs more suitable name delete_sets = self.get_delete_sets() - for k, v in sets.items(): - for i in v: - delete_sets.delete_soft_deleted(k, i) + for key, value in sets.items(): + for i in value: + delete_sets.delete_soft_deleted(key, i) self.set_delete_sets(delete_sets) diff --git a/serverauditor_sshconfig/core/utils.py b/serverauditor_sshconfig/core/utils.py index a1d2cd2..f1e15df 100644 --- a/serverauditor_sshconfig/core/utils.py +++ b/serverauditor_sshconfig/core/utils.py @@ -1,53 +1,6 @@ # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - import os -from six import PY2, PY3 - - -if PY2: - p_input = raw_input - p_map = map - - def to_bytes(s): - if isinstance(s, str): - return s - if isinstance(s, unicode): - return s.encode('utf-8') - - to_str = to_bytes - - def bchr(s): - return chr(s) - - def bord(s): - return ord(s) - -elif PY3: - p_input = input - p_map = lambda f, it: list(map(f, it)) - - def to_bytes(s): - if isinstance(s, bytes): - return s - if isinstance(s, str): - return s.encode('utf-8') - - def to_str(s): - if isinstance(s, bytes): - return s.decode('utf-8') - if isinstance(s, str): - return s - - def bchr(s): - return bytes([s]) - - def bord(s): - return s def expand_and_format_path(paths, **kwargs): diff --git a/serverauditor_sshconfig/sync/services/base.py b/serverauditor_sshconfig/sync/services/base.py index 9826a9b..ff9ce7c 100644 --- a/serverauditor_sshconfig/sync/services/base.py +++ b/serverauditor_sshconfig/sync/services/base.py @@ -1,12 +1,14 @@ import abc import six +from ...core.storage import ApplicationStorage @six.add_metaclass(abc.ABCMeta) class BaseSyncService(object): - def __init__(self, crendetial): + def __init__(self, application_name, crendetial): self.crendetial = crendetial + self.storage = ApplicationStorage(application_name) @abc.abstractmethod def hosts(self): @@ -14,3 +16,6 @@ def hosts(self): def sync(self): service_hosts = self.hosts() + with self.storage: + for i in service_hosts: + self.storage.save(i) From 59740e195a371b048e2d6e067e095ed59cfe2959 Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 00:03:49 +0600 Subject: [PATCH 03/10] Fix issues for cryptor. --- serverauditor_sshconfig/cloud/cryptor.py | 89 ++++++++++++++---------- serverauditor_sshconfig/core/models.py | 2 + 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/serverauditor_sshconfig/cloud/cryptor.py b/serverauditor_sshconfig/cloud/cryptor.py index a4e9b56..b4edb3e 100644 --- a/serverauditor_sshconfig/cloud/cryptor.py +++ b/serverauditor_sshconfig/cloud/cryptor.py @@ -1,16 +1,10 @@ -#!/usr/bin/env python # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - from __future__ import print_function import base64 import hashlib -import hmac +import hmac as python_hmac from Crypto.Cipher import AES from Crypto.Protocol import KDF @@ -24,7 +18,8 @@ class CryptorException(Exception): class RNCryptor(object): - """ + """RNCryptor is a symmetric-key encryption schema. + NB. You must set encryption_salt, hmac_salt and password after creation of RNCryptor's instance. """ @@ -33,49 +28,64 @@ class RNCryptor(object): AES_MODE = AES.MODE_CBC SALT_SIZE = 8 + def __init__(self): + self._password = None + self._encryption_salt = None + self._hmac_salt = None + self._encryption_key = None + self._hmac_key = None + + # pylint: disable=no-self-use def pre_decrypt_data(self, data): data = to_bytes(data) return base64.decodestring(data) + # pylint: disable=no-self-use def post_decrypt_data(self, data): - """ Removes useless symbols which appear over padding for AES (PKCS#7). """ + """Remove useless symbols. + Its appear over padding for AES (PKCS#7). + """ data = data[:-bord(data[-1])] return to_str(data) def decrypt(self, data): data = self.pre_decrypt_data(data) - n = len(data) + length = len(data) - version = data[0] - options = data[1] + # version = data[0] + # options = data[1] encryption_salt = data[2:10] hmac_salt = data[10:18] - iv = data[18:34] - cipher_text = data[34:n - 32] - hmac = data[n - 32:] + initialization_vector = data[18:34] + cipher_text = data[34:length - 32] + hmac = data[length - 32:] - if encryption_salt != self.encryption_salt or hmac_salt != self.hmac_salt: + if ((encryption_salt != self.encryption_salt or + hmac_salt != self.hmac_salt)): raise CryptorException("Bad encryption salt or hmac salt!") encryption_key = self.encryption_key hmac_key = self.hmac_key - if self._hmac(hmac_key, data[:n - 32]) != hmac: + if self._hmac(hmac_key, data[:length - 32]) != hmac: raise CryptorException("Bad data!") - decrypted_data = self._aes_decrypt(encryption_key, iv, cipher_text) + decrypted_data = self._aes_decrypt( + encryption_key, initialization_vector, cipher_text + ) return self.post_decrypt_data(decrypted_data) + # pylint: disable=no-self-use def pre_encrypt_data(self, data): - """ Does padding for the data for AES (PKCS#7). """ - + """Do padding for the data for AES (PKCS#7).""" data = to_bytes(data) rem = self.AES_BLOCK_SIZE - len(data) % self.AES_BLOCK_SIZE return data + bchr(rem) * rem + # pylint: disable=no-self-use def post_encrypt_data(self, data): data = base64.encodestring(data) return to_str(data) @@ -89,13 +99,19 @@ def encrypt(self, data): hmac_salt = self.hmac_salt hmac_key = self.hmac_key - iv = self.iv - cipher_text = self._aes_encrypt(encryption_key, iv, data) + initialization_vector = self.initialization_vector + cipher_text = self._aes_encrypt( + encryption_key, initialization_vector, data + ) version = b'\x02' options = b'\x01' - new_data = b''.join([version, options, encryption_salt, hmac_salt, iv, cipher_text]) + new_data = b''.join([ + version, options, + encryption_salt, hmac_salt, + initialization_vector, cipher_text + ]) encrypted_data = new_data + self._hmac(hmac_key, new_data) return self.post_encrypt_data(encrypted_data) @@ -125,13 +141,15 @@ def hmac_salt(self, value): self._hmac_salt = to_bytes(value) @property - def iv(self): + def initialization_vector(self): return Random.new().read(self.AES_BLOCK_SIZE) @property def encryption_key(self): if not getattr(self, '_encryption_key', None): - self._encryption_key = self._pbkdf2(self.password, self.encryption_salt) + self._encryption_key = self._pbkdf2( + self.password, self.encryption_salt + ) return self._encryption_key @property @@ -140,24 +158,19 @@ def hmac_key(self): self._hmac_key = self._pbkdf2(self.password, self.hmac_salt) return self._hmac_key - def _aes_encrypt(self, key, iv, text): - return AES.new(key, self.AES_MODE, iv).encrypt(text) + def _aes_encrypt(self, key, initialization_vector, text): + return AES.new(key, self.AES_MODE, initialization_vector).encrypt(text) - def _aes_decrypt(self, key, iv, text): - return AES.new(key, self.AES_MODE, iv).decrypt(text) + def _aes_decrypt(self, key, initialization_vector, text): + return AES.new(key, self.AES_MODE, initialization_vector).decrypt(text) def _hmac(self, key, data): - return hmac.new(key, data, hashlib.sha256).digest() + return python_hmac.new(key, data, hashlib.sha256).digest() def _pbkdf2(self, password, salt, iterations=10000, key_length=32): return KDF.PBKDF2(password, salt, dkLen=key_length, count=iterations, - prf=lambda p, s: hmac.new(p, s, hashlib.sha1).digest()) + prf=preudorandom) - ## django 1.5 version -- faster than crypto version - # import hashlib - # from django.utils.crypto import pbkdf2 - # return pbkdf2(password, salt, iterations, dklen=key_length, digest=hashlib.sha1) - ## passlib version -- the fastest version - # from passlib.utils.pbkdf2 import pbkdf2 - # return pbkdf2(password, salt, iterations, key_length) +def preudorandom(password, salt): + return python_hmac.new(password, salt, hashlib.sha1).digest() diff --git a/serverauditor_sshconfig/core/models.py b/serverauditor_sshconfig/core/models.py index 669cc27..7d40fc4 100644 --- a/serverauditor_sshconfig/core/models.py +++ b/serverauditor_sshconfig/core/models.py @@ -85,10 +85,12 @@ def fk_field_names(cls): def mark_updated(self): if self.remote_instance: + # pylint: disable=attribute-defined-outside-init self.remote_instance.state = 'updated' def mark_synced(self): if self.remote_instance: + # pylint: disable=attribute-defined-outside-init self.remote_instance.state = 'synced' # set_name = '' From a7f46690a6e7721be6c0972b7d90c3e31d958169 Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 00:17:56 +0600 Subject: [PATCH 04/10] Fix some issues for ssh_config module. --- serverauditor_sshconfig/core/ssh_config.py | 73 +++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/serverauditor_sshconfig/core/ssh_config.py b/serverauditor_sshconfig/core/ssh_config.py index 74218b0..268d5f6 100644 --- a/serverauditor_sshconfig/core/ssh_config.py +++ b/serverauditor_sshconfig/core/ssh_config.py @@ -1,10 +1,5 @@ # coding: utf-8 -""" -Copyright (c) 2013 Crystalnix. -License BSD, see LICENSE for more details. -""" - import fnmatch import getpass import os @@ -17,7 +12,7 @@ class SSHConfigException(Exception): class SSHConfig(object): - """ Representation of ssh config information. + """Representation of ssh config information. For information about the format see OpenSSH's man page. Based on paramiko.config.SSHConfig. @@ -31,48 +26,49 @@ def __init__(self): self._config = [{'host': ['*']}] def _is_file_ok(self, path): - """ Checks that file exists, and user have permissions for read it. + """Check that file exists, and user have permissions for read it. :param path: path where file is located. :return: True or False. """ - - return os.path.exists(path) and not os.path.isdir(path) and os.access(path, os.R_OK) + return ( + os.path.exists(path) and + not os.path.isdir(path) and + os.access(path, os.R_OK) + ) def parse(self): - """ Parses configuration files. + """Parse configuration files. Firstly, parser uses file which is located in USER_CONFIG_PATH. Then, file SYSTEM_CONFIG_PATH is used. """ - - def create_config_file(): - """ Creates configuration file. """ + def create_config_file(path): + """Create configuration file.""" ssh_dir = os.path.dirname(path) if not os.path.exists(ssh_dir): os.mkdir(ssh_dir, 0o700) - with open(path, 'w') as f: - f.write("# File was created by ServerAuditor\n\n") + with open(path, 'w') as _file: + _file.write("# File was created by ServerAuditor\n\n") return for path in (self.USER_CONFIG_PATH, ): # self.SYSTEM_CONFIG_PATH): if not self._is_file_ok(path): - create_config_file() + create_config_file(path) else: - with open(path) as f: - self._parse_file(f) + with open(path) as _file: + self._parse_file(_file) return def _parse_file(self, file_object): - """ Parses separated file. + """Parse separated file. :raises SSHConfigException: if there is any unparsable line in file. :param file_object: file. """ - def get_hosts(val): i, length = 0, len(val) hosts = [] @@ -122,33 +118,36 @@ def get_hosts(val): return def get_complete_hosts(self): - """ Returns complete hosts. + """Return complete hosts. :return: list of hosts which names don't have masks. """ - def is_complete(conf): - """ Checks that host is complete. + """Check that host is complete. :param conf: config. :return: True or False """ host = conf['host'] - is_full_name = len(host) == 1 and '*' not in host[0] and '?' not in host[0] and not host[0].startswith('!') + is_full_name = ( + len(host) == 1 and + '*' not in host[0] and + '?' not in host[0] and + not host[0].startswith('!') + ) return is_full_name return [conf['host'][0] for conf in self._config if is_complete(conf)] def get_host(self, host, substitute=False): - """ Returns config for host. + """Return config for host. :param host: host's name :param substitute: if True all patterns will be substituted. :return: config for host. """ - def is_match(patterns): - """ Checks that host applies patterns. + """Check that host applies patterns. :param patterns: list of patterns. :return: True or False @@ -179,14 +178,16 @@ def is_match(patterns): return settings def _substitute_variables(self, settings): - """ Substitutes variables in settings. + """Substitute variables in settings. - :raises SSHConfigException: if user does not permissions for read IdentityFile. + :raises SSHConfigException: when user does + not permissions for read IdentityFile. :param settings: config which will be changed. """ - if 'hostname' in settings: - settings['hostname'] = settings['hostname'].replace('%h', settings['host']) + settings['hostname'] = settings['hostname'].replace( + '%h', settings['host'] + ) else: settings['hostname'] = settings['host'] @@ -244,7 +245,9 @@ def _substitute_variables(self, settings): for find, replace in replacements[k]: if isinstance(settings[k], list): for i in range(len(settings[k])): - settings[k][i] = settings[k][i].replace(find, replace) + settings[k][i] = settings[k][i].replace( + find, replace + ) else: settings[k] = settings[k].replace(find, replace) @@ -252,9 +255,7 @@ def _substitute_variables(self, settings): for i, name in enumerate(settings['identityfile']): if self._is_file_ok(name): name = name[name.rfind('/') + 1:] - with open(settings['identityfile'][i]) as f: - settings['identityfile'][i] = [name, f.read()] - #else: - # raise SSHConfigException('Can not read IdentityFile %s' % settings['identityfile']) + with open(settings['identityfile'][i]) as _file: + settings['identityfile'][i] = [name, _file.read()] return From d24e9b0d7255a50b1da55a68d0b88ffec23ce3e0 Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 09:24:29 +0600 Subject: [PATCH 05/10] Fix no-self-use issues. --- serverauditor_sshconfig/account/commands.py | 1 + serverauditor_sshconfig/cloud/commands/group.py | 1 + serverauditor_sshconfig/cloud/commands/host.py | 1 + serverauditor_sshconfig/cloud/commands/pf_rule.py | 1 + serverauditor_sshconfig/cloud/commands/snippet.py | 1 + .../cloud/commands/ssh_config.py | 1 + .../cloud/commands/ssh_identity.py | 1 + serverauditor_sshconfig/cloud/controllers.py | 1 + serverauditor_sshconfig/core/commands.py | 2 ++ serverauditor_sshconfig/core/ssh_config.py | 13 +++++++------ serverauditor_sshconfig/core/storage/strategies.py | 1 + 11 files changed, 18 insertions(+), 6 deletions(-) diff --git a/serverauditor_sshconfig/account/commands.py b/serverauditor_sshconfig/account/commands.py index d5fb292..c24bd84 100644 --- a/serverauditor_sshconfig/account/commands.py +++ b/serverauditor_sshconfig/account/commands.py @@ -15,6 +15,7 @@ def __init__(self, app, app_args, cmd_name=None): class LoginCommand(BaseAccountCommand): """Sign into serverauditor cloud.""" + # pylint: disable=no-self-use def prompt_username(self): return six.moves.input("Serverauditor's username: ") diff --git a/serverauditor_sshconfig/cloud/commands/group.py b/serverauditor_sshconfig/cloud/commands/group.py index 25408ec..8b66a31 100644 --- a/serverauditor_sshconfig/cloud/commands/group.py +++ b/serverauditor_sshconfig/cloud/commands/group.py @@ -31,6 +31,7 @@ def get_parser(self, prog_name): def create(self, parsed_args): self.create_instance(parsed_args) + # pylint: disable=no-self-use def serialize_args(self, args, instance=None): if instance: ssh_identity = ( diff --git a/serverauditor_sshconfig/cloud/commands/host.py b/serverauditor_sshconfig/cloud/commands/host.py index e67d66e..f3d4b88 100644 --- a/serverauditor_sshconfig/cloud/commands/host.py +++ b/serverauditor_sshconfig/cloud/commands/host.py @@ -44,6 +44,7 @@ def create(self, parsed_args): self.create_instance(parsed_args) + # pylint: disable=no-self-use def serialize_args(self, args, instance=None): if instance: ssh_identity = ( diff --git a/serverauditor_sshconfig/cloud/commands/pf_rule.py b/serverauditor_sshconfig/cloud/commands/pf_rule.py index 15e0a9a..c627bda 100644 --- a/serverauditor_sshconfig/cloud/commands/pf_rule.py +++ b/serverauditor_sshconfig/cloud/commands/pf_rule.py @@ -15,6 +15,7 @@ class PFRuleCommand(DetailCommand): model_class = PFRule @property + # pylint: disable=no-self-use def binding_parsers(self): return { 'D': BindingParser.dynamic, diff --git a/serverauditor_sshconfig/cloud/commands/snippet.py b/serverauditor_sshconfig/cloud/commands/snippet.py index 4120142..2012d53 100644 --- a/serverauditor_sshconfig/cloud/commands/snippet.py +++ b/serverauditor_sshconfig/cloud/commands/snippet.py @@ -28,6 +28,7 @@ def create(self, parsed_args): self.create_instance(parsed_args) + # pylint: disable=no-self-use def serialize_args(self, args, instance=None): if instance: snippet = instance diff --git a/serverauditor_sshconfig/cloud/commands/ssh_config.py b/serverauditor_sshconfig/cloud/commands/ssh_config.py index 6ccf31a..ee18871 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_config.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_config.py @@ -1,6 +1,7 @@ class SshConfigArgs(object): + # pylint: disable=no-self-use def add_agrs(self, parser): parser.add_argument( '-p', '--port', diff --git a/serverauditor_sshconfig/cloud/commands/ssh_identity.py b/serverauditor_sshconfig/cloud/commands/ssh_identity.py index 6f61386..7180fe6 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_identity.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_identity.py @@ -40,6 +40,7 @@ def get_parser(self, prog_name): def create(self, parsed_args): self.create_instance(parsed_args) + # pylint: disable=no-self-use def serialize_args(self, args, instance=None): if instance: identity = instance diff --git a/serverauditor_sshconfig/cloud/controllers.py b/serverauditor_sshconfig/cloud/controllers.py index 2d372cd..0f7cdf4 100644 --- a/serverauditor_sshconfig/cloud/controllers.py +++ b/serverauditor_sshconfig/cloud/controllers.py @@ -7,6 +7,7 @@ class CryptoController(object): def __init__(self, cryptor): self.cryptor = cryptor + # pylint: disable=no-self-use def _mutate_fields(self, model, mutator): for i in model.crypto_fields: crypto_field = getattr(model, i) diff --git a/serverauditor_sshconfig/core/commands.py b/serverauditor_sshconfig/core/commands.py index 9bca2af..9ffef90 100644 --- a/serverauditor_sshconfig/core/commands.py +++ b/serverauditor_sshconfig/core/commands.py @@ -20,6 +20,7 @@ class PasswordPromptMixin(object): + # pylint: disable=no-self-use def prompt_password(self): return getpass.getpass("Serverauditor's password:") @@ -153,6 +154,7 @@ def get_objects(self, ids__names): raise DoesNotExistException("There aren't any instance.") return instances + # pylint: disable=no-self-use def parse_ids_names(self, ids__names): ids = [int(i) for i in ids__names if i.isdigit()] return ids, ids__names diff --git a/serverauditor_sshconfig/core/ssh_config.py b/serverauditor_sshconfig/core/ssh_config.py index 268d5f6..45e5bb2 100644 --- a/serverauditor_sshconfig/core/ssh_config.py +++ b/serverauditor_sshconfig/core/ssh_config.py @@ -25,6 +25,7 @@ class SSHConfig(object): def __init__(self): self._config = [{'host': ['*']}] + # pylint: disable=no-self-use def _is_file_ok(self, path): """Check that file exists, and user have permissions for read it. @@ -165,13 +166,13 @@ def is_match(patterns): matches = [h for h in self._config if is_match(h['host'])] settings = {'host': host} - for m in matches: - for k, v in m.items(): - if k not in settings: - if isinstance(v, list): - settings[k] = v[:] + for match in matches: + for key, value in match.items(): + if key not in settings: + if isinstance(value, list): + settings[key] = value[:] else: - settings[k] = v + settings[key] = value if substitute: self._substitute_variables(settings) diff --git a/serverauditor_sshconfig/core/storage/strategies.py b/serverauditor_sshconfig/core/storage/strategies.py index 83b37cb..46602b4 100644 --- a/serverauditor_sshconfig/core/storage/strategies.py +++ b/serverauditor_sshconfig/core/storage/strategies.py @@ -11,6 +11,7 @@ def __init__(self, storage): class SaveStrategy(Strategy): + # pylint: disable=no-self-use def save_submodel(self, submodel, mapping): if isinstance(submodel, six.integer_types): return submodel From 10c86f5e831fbaf2866ea511cce81567cdc1a98f Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 10:38:35 +0600 Subject: [PATCH 06/10] Fix unused argument issues. Add asserts to not completed features and turn off lint. --- serverauditor_sshconfig/app.py | 1 + .../cloud/commands/__init__.py | 1 + .../cloud/commands/group.py | 33 ++++++++----------- .../cloud/commands/host.py | 32 ++++++++---------- .../cloud/commands/pf_rule.py | 1 + .../cloud/commands/snippet.py | 1 + .../cloud/commands/ssh_config.py | 22 +++++++++++++ .../cloud/commands/ssh_identity.py | 1 + serverauditor_sshconfig/cloud/commands/tag.py | 2 ++ serverauditor_sshconfig/cloud/serializers.py | 2 +- serverauditor_sshconfig/core/api.py | 1 + serverauditor_sshconfig/core/commands.py | 7 ++-- serverauditor_sshconfig/core/models.py | 8 +++-- .../core/storage/__init__.py | 25 +++++++++----- .../core/storage/driver.py | 4 +-- .../core/storage/idgenerators.py | 2 ++ serverauditor_sshconfig/core/storage/query.py | 2 ++ .../core/storage/strategies.py | 3 +- 18 files changed, 89 insertions(+), 59 deletions(-) diff --git a/serverauditor_sshconfig/app.py b/serverauditor_sshconfig/app.py index 64bf9c4..57d85b4 100644 --- a/serverauditor_sshconfig/app.py +++ b/serverauditor_sshconfig/app.py @@ -8,6 +8,7 @@ from . import get_version +# pylint: disable=too-few-public-methods class ServerauditorApp(object, App): def __init__(self): diff --git a/serverauditor_sshconfig/cloud/commands/__init__.py b/serverauditor_sshconfig/cloud/commands/__init__.py index d2887bd..457bc45 100644 --- a/serverauditor_sshconfig/cloud/commands/__init__.py +++ b/serverauditor_sshconfig/cloud/commands/__init__.py @@ -35,6 +35,7 @@ def get_parser(self, prog_name): parser.add_argument('id_or_name', metavar='ID or NAME') return parser + # pylint: disable=unused-argument def take_action(self, parsed_args): self.log.info('Info about group or host.') assert False, 'Not implemented' diff --git a/serverauditor_sshconfig/cloud/commands/group.py b/serverauditor_sshconfig/cloud/commands/group.py index 8b66a31..6a18f46 100644 --- a/serverauditor_sshconfig/cloud/commands/group.py +++ b/serverauditor_sshconfig/cloud/commands/group.py @@ -1,6 +1,6 @@ from operator import attrgetter from ...core.commands import DetailCommand, ListCommand -from ..models import Group, SshConfig, SshIdentity +from ..models import Group from .ssh_config import SshConfigArgs @@ -10,6 +10,10 @@ class GroupCommand(DetailCommand): allowed_operations = DetailCommand.all_operations model_class = Group + def __init__(self, *args, **kwargs): + super(GroupCommand, self).__init__(self, *args, **kwargs) + self.ssh_config_args = SshConfigArgs() + def get_parser(self, prog_name): parser = super(GroupCommand, self).get_parser(prog_name) parser.add_argument( @@ -24,8 +28,7 @@ def get_parser(self, prog_name): metavar='PARENT_GROUP', help="Parent group's id or name." ) - ssh_config_args = SshConfigArgs() - ssh_config_args.add_agrs(parser) + self.ssh_config_args.add_agrs(parser) return parser def create(self, parsed_args): @@ -34,28 +37,16 @@ def create(self, parsed_args): # pylint: disable=no-self-use def serialize_args(self, args, instance=None): if instance: - ssh_identity = ( - instance.ssh_config and instance.ssh_config.ssh_identity - ) or SshIdentity() - ssh_config = instance.ssh_config or SshConfig() + ssh_config = self.ssh_config_args.serialize_args( + args, instance.ssh_config + ) group = instance else: - group, ssh_config, ssh_identity = ( - Group(), SshConfig(), SshIdentity() - ) + group = Group() + ssh_config = self.ssh_config_args.serialize_args(args, None) - if args.generate_key: - raise NotImplementedError('Not implemented') if args.parent_group: raise NotImplementedError('Not implemented') - if args.ssh_identity: - raise NotImplementedError('Not implemented') - - ssh_identity.username = args.username - ssh_identity.password = args.password - - ssh_config.port = args.port - ssh_config.ssh_identity = ssh_identity group.label = args.label group.ssh_config = ssh_config @@ -78,7 +69,9 @@ def get_parser(self, prog_name): ) return parser + # pylint: disable=unused-argument def take_action(self, parsed_args): + assert False, 'Filtering and recursive not implemented.' groups = self.storage.get_all(Group) fields = Group.allowed_fields() getter = attrgetter(*fields) diff --git a/serverauditor_sshconfig/cloud/commands/host.py b/serverauditor_sshconfig/cloud/commands/host.py index f3d4b88..85bd460 100644 --- a/serverauditor_sshconfig/cloud/commands/host.py +++ b/serverauditor_sshconfig/cloud/commands/host.py @@ -1,7 +1,7 @@ from operator import attrgetter from ...core.exceptions import ArgumentRequiredException from ...core.commands import DetailCommand, ListCommand -from ..models import Host, SshConfig, SshIdentity +from ..models import Host from .ssh_config import SshConfigArgs @@ -11,6 +11,10 @@ class HostCommand(DetailCommand): allowed_operations = DetailCommand.all_operations model_class = Host + def __init__(self, *args, **kwargs): + super(HostCommand, self).__init__(self, *args, **kwargs) + self.ssh_config_args = SshConfigArgs() + def get_parser(self, prog_name): parser = super(HostCommand, self).get_parser(prog_name) parser.add_argument( @@ -34,8 +38,7 @@ def get_parser(self, prog_name): metavar='ADDRESS', help='Address of host.' ) - ssh_config_args = SshConfigArgs() - ssh_config_args.add_agrs(parser) + self.ssh_config_args.add_agrs(parser) return parser def create(self, parsed_args): @@ -47,30 +50,19 @@ def create(self, parsed_args): # pylint: disable=no-self-use def serialize_args(self, args, instance=None): if instance: - ssh_identity = ( - instance.ssh_config and instance.ssh_config.ssh_identity - ) or SshIdentity() - ssh_config = instance.ssh_config or SshConfig() + ssh_config = self.ssh_config_args.serialize_args( + args, instance.ssh_config + ) host = instance else: - host, ssh_config, ssh_identity = Host(), SshConfig(), SshIdentity() - - if args.generate_key: - raise NotImplementedError('Not implemented') + host = Host() + ssh_config = self.ssh_config_args.serialize_args(args, None) if args.group: raise NotImplementedError('Not implemented') - if args.ssh_identity: - raise NotImplementedError('Not implemented') - - ssh_identity.username = args.username - ssh_identity.password = args.password - ssh_config.port = args.port host.label = args.label host.address = args.address - - ssh_config.ssh_identity = ssh_identity host.ssh_config = ssh_config return host @@ -90,7 +82,9 @@ def get_parser(self, prog_name): ) return parser + # pylint: disable=unused-argument def take_action(self, parsed_args): + assert False, 'Filtering by tags and groups not implemented.' hosts = self.storage.get_all(Host) fields = Host.allowed_fields() getter = attrgetter(*fields) diff --git a/serverauditor_sshconfig/cloud/commands/pf_rule.py b/serverauditor_sshconfig/cloud/commands/pf_rule.py index c627bda..c769713 100644 --- a/serverauditor_sshconfig/cloud/commands/pf_rule.py +++ b/serverauditor_sshconfig/cloud/commands/pf_rule.py @@ -94,6 +94,7 @@ def get_host(self, arg): class PFRulesCommand(ListCommand): """Manage port forwarding rule objects.""" + # pylint: disable=unused-argument def take_action(self, parsed_args): pf_rules = self.storage.get_all(PFRule) fields = PFRule.allowed_fields() diff --git a/serverauditor_sshconfig/cloud/commands/snippet.py b/serverauditor_sshconfig/cloud/commands/snippet.py index 2012d53..f6a4556 100644 --- a/serverauditor_sshconfig/cloud/commands/snippet.py +++ b/serverauditor_sshconfig/cloud/commands/snippet.py @@ -43,6 +43,7 @@ def serialize_args(self, args, instance=None): class SnippetsCommand(ListCommand): """Manage snippet objects.""" + # pylint: disable=unused-argument def take_action(self, parsed_args): groups = self.storage.get_all(Snippet) fields = Snippet.allowed_fields() diff --git a/serverauditor_sshconfig/cloud/commands/ssh_config.py b/serverauditor_sshconfig/cloud/commands/ssh_config.py index ee18871..4e60667 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_config.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_config.py @@ -1,3 +1,5 @@ +from ..models import SshConfig, SshIdentity + class SshConfigArgs(object): @@ -43,3 +45,23 @@ def add_agrs(self, parser): help='Create and assign automatically snippet.' ) return parser + + def serialize_args(self, args, instance): + if instance: + ssh_identity = ( + instance.ssh_identity + ) or SshIdentity() + ssh_config = instance or SshConfig() + else: + ssh_config, ssh_identity = SshConfig(), SshIdentity() + + if args.generate_key: + raise NotImplementedError('Not implemented') + + if args.ssh_identity: + raise NotImplementedError('Not implemented') + + ssh_identity.username = args.username + ssh_identity.password = args.password + ssh_config.port = args.port + return ssh_config diff --git a/serverauditor_sshconfig/cloud/commands/ssh_identity.py b/serverauditor_sshconfig/cloud/commands/ssh_identity.py index 7180fe6..c26f40c 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_identity.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_identity.py @@ -64,6 +64,7 @@ def serialize_args(self, args, instance=None): class SshIdentitiesCommand(ListCommand): """Manage ssh identity objects.""" + # pylint: disable=unused-argument def take_action(self, parsed_args): ssh_identities = self.storage.get_all(SshIdentity) fields = SshIdentity.allowed_fields() diff --git a/serverauditor_sshconfig/cloud/commands/tag.py b/serverauditor_sshconfig/cloud/commands/tag.py index bd7b6a6..e6d38ae 100644 --- a/serverauditor_sshconfig/cloud/commands/tag.py +++ b/serverauditor_sshconfig/cloud/commands/tag.py @@ -16,5 +16,7 @@ def get_parser(self, prog_name): ) return parser + # pylint: disable=unused-argument def take_action(self, parsed_args): self.log.info('Tag objects.') + assert False, 'Not implemented' diff --git a/serverauditor_sshconfig/cloud/serializers.py b/serverauditor_sshconfig/cloud/serializers.py index 501a52c..1806ca5 100644 --- a/serverauditor_sshconfig/cloud/serializers.py +++ b/serverauditor_sshconfig/cloud/serializers.py @@ -207,7 +207,7 @@ def to_model(self, payload): try: deleted_set.append(serializer.to_model(i)) except DoesNotExistException: - pass + continue models['deleted_sets'][set_name] = deleted_set self.process_model_entries( diff --git a/serverauditor_sshconfig/core/api.py b/serverauditor_sshconfig/core/api.py index 34378ea..e551835 100644 --- a/serverauditor_sshconfig/core/api.py +++ b/serverauditor_sshconfig/core/api.py @@ -7,6 +7,7 @@ from requests.auth import AuthBase +# pylint: disable=too-few-public-methods class ServerauditorAuth(AuthBase): header_name = 'Authorization' diff --git a/serverauditor_sshconfig/core/commands.py b/serverauditor_sshconfig/core/commands.py index 9ffef90..31b65e9 100644 --- a/serverauditor_sshconfig/core/commands.py +++ b/serverauditor_sshconfig/core/commands.py @@ -19,6 +19,7 @@ ) +# pylint: disable=too-few-public-methods class PasswordPromptMixin(object): # pylint: disable=no-self-use def prompt_password(self): @@ -177,6 +178,7 @@ def delete_instance(self, instance): self.log_delete(instance) +# pylint: disable=too-few-public-methods class ListCommand(object, Lister): log = logging.getLogger(__name__) @@ -188,10 +190,5 @@ def __init__(self, app, app_args, cmd_name=None): def get_parser(self, prog_name): parser = super(ListCommand, self).get_parser(prog_name) - parser.add_argument( - '-l', '--list', action='store_true', - help=('List hosts in current group with id, name, group in path ' - 'format, tags, username, address and port.') - ) parser.add_argument('--log-file', help="Path to log file.") return parser diff --git a/serverauditor_sshconfig/core/models.py b/serverauditor_sshconfig/core/models.py index 7d40fc4..a0e376a 100644 --- a/serverauditor_sshconfig/core/models.py +++ b/serverauditor_sshconfig/core/models.py @@ -47,6 +47,7 @@ def __copy__(self): newone.update(self) return newone + # pylint: disable=unused-argument def __deepcopy__(self, requesteddeepcopy): return type(self)(copy.deepcopy(super(AbstractModel, self))) @@ -69,12 +70,15 @@ class Model(AbstractModel): } def __init__(self, *args, **kwargs): + # The simplest way to make lint not raise + # access-member-before-definition + self.remote_instance = None super(Model, self).__init__(*args, **kwargs) - is_need_to_patch_remote_instance = ( + is_need_to_patch_remote = ( self.remote_instance and not isinstance(self.remote_instance, RemoteInstance) ) - if is_need_to_patch_remote_instance: + if is_need_to_patch_remote: self.remote_instance = RemoteInstance(self.remote_instance) @classmethod diff --git a/serverauditor_sshconfig/core/storage/__init__.py b/serverauditor_sshconfig/core/storage/__init__.py index 5a09bfe..bd9f6f4 100644 --- a/serverauditor_sshconfig/core/storage/__init__.py +++ b/serverauditor_sshconfig/core/storage/__init__.py @@ -1,3 +1,4 @@ +from collections import namedtuple from .idgenerators import UUIDGenerator from .driver import PersistentDict from ..utils import expand_and_format_path @@ -6,6 +7,7 @@ from .query import Query +# pylint: disable=too-few-public-methods class InternalModelContructor(object): def __init__(self, strategy): @@ -15,6 +17,7 @@ def __call__(self, raw_data, model_class): return model_class(raw_data) +# pylint: disable=too-few-public-methods class ModelContructor(InternalModelContructor): def __call__(self, raw_data, model_class): @@ -22,6 +25,9 @@ def __call__(self, raw_data, model_class): return self.strategy.get(model) +Strategies = namedtuple('Strategies', ('getter', 'saver', 'deleter')) + + class ApplicationStorage(object): path = '~/.{application_name}.storage' @@ -35,15 +41,16 @@ def __init__(self, application_name, save_strategy=None, self.driver = PersistentDict(self._path) self.id_generator = UUIDGenerator(self) - self.save_strategy = self.make_strategy(save_strategy, SaveStrategy) - self.get_strategy = self.make_strategy(get_strategy, GetStrategy) - self.delete_strategy = self.make_strategy(delete_strategy, - DeleteStrategy) + self.strategies = Strategies( + self.make_strategy(get_strategy, GetStrategy), + self.make_strategy(save_strategy, SaveStrategy), + self.make_strategy(delete_strategy, DeleteStrategy) + ) self.internal_model_constructor = InternalModelContructor( - self.get_strategy) + self.strategies.getter) self.model_constructor = ModelContructor( - self.get_strategy) + self.strategies.getter) def make_strategy(self, strategy_class, default): strategy_class = strategy_class or default @@ -64,7 +71,7 @@ def save(self, model): Will return model with id and saved mapped fields Model instances with ids. """ - model = self.save_strategy.save(model) + model = self.strategy.saver.save(model) if getattr(model, model.id_name): saved_model = self.update(model) else: @@ -86,11 +93,11 @@ def update(self, model): def delete(self, model): self._internal_delete(model) - self.delete_strategy.delete(model) + self.strategies.deleter.delete(model) def confirm_delete(self, deleted_sets): # FIXME It needs more suitable name - self.delete_strategy.confirm_delete(deleted_sets) + self.strategies.deleter.confirm_delete(deleted_sets) def get(self, model_class, query_union=None, **kwargs): founded_models = self.filter(model_class, query_union, **kwargs) diff --git a/serverauditor_sshconfig/core/storage/driver.py b/serverauditor_sshconfig/core/storage/driver.py index e3675cb..804fdc5 100644 --- a/serverauditor_sshconfig/core/storage/driver.py +++ b/serverauditor_sshconfig/core/storage/driver.py @@ -129,6 +129,6 @@ def load(self, fileobj): for loader in DRIVERS.values(): try: return self.update(loader.load(fileobj)) - except Exception: - pass + except Exception: # pylint: disable=broad-except + continue raise ValueError('File not in a supported format') diff --git a/serverauditor_sshconfig/core/storage/idgenerators.py b/serverauditor_sshconfig/core/storage/idgenerators.py index c6833b1..f785196 100644 --- a/serverauditor_sshconfig/core/storage/idgenerators.py +++ b/serverauditor_sshconfig/core/storage/idgenerators.py @@ -1,6 +1,7 @@ from uuid import uuid4 +# pylint: disable=too-few-public-methods class UUIDGenerator(object): def __init__(self, storage): @@ -8,6 +9,7 @@ def __init__(self, storage): :param ApplicationStorage storage: Storage instance """ + self.storage = storage def __call__(self, model): """Generate id. diff --git a/serverauditor_sshconfig/core/storage/query.py b/serverauditor_sshconfig/core/storage/query.py index 25250bb..6a751c1 100644 --- a/serverauditor_sshconfig/core/storage/query.py +++ b/serverauditor_sshconfig/core/storage/query.py @@ -1,6 +1,7 @@ from . import operators +# pylint: disable=too-few-public-methods class QueryOperator(object): operators = ['eq', 'ne', 'gt', 'lt', 'le', 'ge', 'rcontains', 'contains'] @@ -25,6 +26,7 @@ def __call__(self, obj): return self.operator(field, self.value) +# pylint: disable=too-few-public-methods class Query(object): def __init__(self, union=None, **kwargs): diff --git a/serverauditor_sshconfig/core/storage/strategies.py b/serverauditor_sshconfig/core/storage/strategies.py index 46602b4..397edb9 100644 --- a/serverauditor_sshconfig/core/storage/strategies.py +++ b/serverauditor_sshconfig/core/storage/strategies.py @@ -11,7 +11,7 @@ def __init__(self, storage): class SaveStrategy(Strategy): - # pylint: disable=no-self-use + # pylint: disable=no-self-use,unused-argument def save_submodel(self, submodel, mapping): if isinstance(submodel, six.integer_types): return submodel @@ -60,6 +60,7 @@ def get(self, model): class DeleteStrategy(Strategy): + # pylint: disable=no-self-use def get_delete_sets(self): return {} From f83ec5ff766612a65c2179fc073475e6bf7fb1e2 Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 10:43:23 +0600 Subject: [PATCH 07/10] Add sshconfig parser to linter ignore list. --- .prospector.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index 4644378..f40d1e9 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -7,6 +7,7 @@ ignore-paths: - .git - setup.py - build + - serverauditor_sshconfig/core/ssh_config.py # it's too complex pep257: disable: @@ -21,8 +22,4 @@ pep257: pylint: options: max-parents: 12 - disable: - # - broad-except - # - pointless-except - # - bad-super-call - # - nonstandard-exception + From 819d5b276814e51c2aa73567c44b4a28016fe5ef Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 10:56:32 +0600 Subject: [PATCH 08/10] Remove pep257 limitations. --- .prospector.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index f40d1e9..c040d56 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -11,12 +11,6 @@ ignore-paths: pep257: disable: - - D100 # TODO remove it when done with code style fixing. - - D101 # TODO remove it when done with code style fixing. - - D102 # TODO remove it when done with code style fixing. - - D103 # TODO remove it when done with code style fixing. - - D104 # TODO remove it when done with code style fixing. - - D105 # TODO remove it when done with code style fixing. - D203 # 1 blank line required before pylint: From f86864415692e39430bd68535f3b67eb44798350 Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 22:14:28 +0600 Subject: [PATCH 09/10] Add docstrings. --- .prospector.yaml | 4 + pavement.py | 11 +- requirements.txt | 2 +- serverauditor_sshconfig/__init__.py | 3 + serverauditor_sshconfig/account/__init__.py | 2 + serverauditor_sshconfig/account/commands.py | 18 ++- serverauditor_sshconfig/account/managers.py | 8 +- serverauditor_sshconfig/app.py | 8 +- serverauditor_sshconfig/cloud/__init__.py | 2 + .../cloud/commands/__init__.py | 7 + .../cloud/commands/group.py | 14 ++ .../cloud/commands/host.py | 14 ++ .../cloud/commands/pf_rule.py | 27 +++- .../cloud/commands/snippet.py | 9 ++ .../cloud/commands/ssh_config.py | 5 + .../cloud/commands/ssh_identity.py | 11 +- .../cloud/commands/sync.py | 13 +- serverauditor_sshconfig/cloud/commands/tag.py | 9 +- serverauditor_sshconfig/cloud/controllers.py | 10 ++ serverauditor_sshconfig/cloud/cryptor.py | 141 ++++++++++-------- serverauditor_sshconfig/cloud/models.py | 11 ++ serverauditor_sshconfig/cloud/serializers.py | 36 ++++- serverauditor_sshconfig/core/__init__.py | 2 + serverauditor_sshconfig/core/api.py | 25 +++- serverauditor_sshconfig/core/commands.py | 68 +++++++-- serverauditor_sshconfig/core/exceptions.py | 12 ++ serverauditor_sshconfig/core/models.py | 26 +++- serverauditor_sshconfig/core/settings.py | 13 +- .../core/storage/__init__.py | 48 +++++- .../core/storage/driver.py | 41 ++++- .../core/storage/idgenerators.py | 3 + .../core/storage/operators.py | 7 +- serverauditor_sshconfig/core/storage/query.py | 8 + .../core/storage/strategies.py | 52 ++++++- serverauditor_sshconfig/core/utils.py | 5 +- serverauditor_sshconfig/handlers.py | 8 +- serverauditor_sshconfig/main.py | 5 +- serverauditor_sshconfig/sync/__init__.py | 6 + serverauditor_sshconfig/sync/commands.py | 20 ++- .../sync/services/__init__.py | 2 + serverauditor_sshconfig/sync/services/base.py | 5 + tox.ini | 2 +- 42 files changed, 585 insertions(+), 138 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index c040d56..d9fba43 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -8,6 +8,10 @@ ignore-paths: - setup.py - build - serverauditor_sshconfig/core/ssh_config.py # it's too complex + - serverauditor_sshconfig/core/cryptor.py # it's too complex + + # TODO Remove this file in production + - serverauditor_sshconfig/sync/services/aws.py pep257: disable: diff --git a/pavement.py b/pavement.py index c892874..1cb67c9 100644 --- a/pavement.py +++ b/pavement.py @@ -1,12 +1,13 @@ -from paver.easy import * # noqa -from paver.setuputils import setup -from setuptools import find_packages - +# -*- coding: utf-8 -*- +"""Config-like for paver tool.""" import sys +from setuptools import find_packages +from paver.easy import task, sh, needs # noqa +from paver.setuputils import setup # noqa sys.path.append('.') +from serverauditor_sshconfig import get_version # noqa -from serverauditor_sshconfig import get_version # pylint: disable=invalid-name requires = [ diff --git a/requirements.txt b/requirements.txt index b50e24b..feded18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pycrypto==2.6 \ No newline at end of file +pycrypto==2.6 diff --git a/serverauditor_sshconfig/__init__.py b/serverauditor_sshconfig/__init__.py index a705a65..03fd7b7 100644 --- a/serverauditor_sshconfig/__init__.py +++ b/serverauditor_sshconfig/__init__.py @@ -1,5 +1,8 @@ +# -*- coding: utf-8 -*- +"""Package with CLI tool and API.""" __version__ = (0, 7, 2) def get_version(): + """Return current version.""" return '.'.join([str(i) for i in __version__]) diff --git a/serverauditor_sshconfig/account/__init__.py b/serverauditor_sshconfig/account/__init__.py index e69de29..9ec8654 100644 --- a/serverauditor_sshconfig/account/__init__.py +++ b/serverauditor_sshconfig/account/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Package with account command set.""" diff --git a/serverauditor_sshconfig/account/commands.py b/serverauditor_sshconfig/account/commands.py index c24bd84..6486ef2 100644 --- a/serverauditor_sshconfig/account/commands.py +++ b/serverauditor_sshconfig/account/commands.py @@ -1,13 +1,16 @@ -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Module to keep login and logout command.""" import six from ..core.commands import AbstractCommand from .managers import AccountManager +# pylint: disable=abstract-method class BaseAccountCommand(AbstractCommand): + """Base class for login and logout commands.""" def __init__(self, app, app_args, cmd_name=None): + """Construct new instance.""" super(BaseAccountCommand, self).__init__(app, app_args, cmd_name) self.manager = AccountManager(self.config) @@ -17,9 +20,14 @@ class LoginCommand(BaseAccountCommand): # pylint: disable=no-self-use def prompt_username(self): + """Ask username prompt.""" return six.moves.input("Serverauditor's username: ") def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(LoginCommand, self).get_parser(prog_name) parser.add_argument('-u', '--username', metavar='USERNAME') parser.add_argument('-p', '--password', metavar='PASSWORD') @@ -27,6 +35,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): + """Process CLI call.""" username = parsed_args.username or self.prompt_username() password = parsed_args.password or self.prompt_password() self.manager.login(username, password) @@ -37,10 +46,15 @@ class LogoutCommand(BaseAccountCommand): """Sign out serverauditor cloud.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(LogoutCommand, self).get_parser(prog_name) parser.add_argument('--clear-sshconfig', action='store_true') return parser def take_action(self, _): + """Process CLI call.""" self.manager.logout() self.log.info('Sign out serverauditor cloud.') diff --git a/serverauditor_sshconfig/account/managers.py b/serverauditor_sshconfig/account/managers.py index bafc018..cdba9c3 100644 --- a/serverauditor_sshconfig/account/managers.py +++ b/serverauditor_sshconfig/account/managers.py @@ -1,15 +1,18 @@ -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Module with Account manager.""" from ..core.api import API class AccountManager(object): + """Class to keep logic for login and logout.""" def __init__(self, config): + """Create new account manager.""" self.config = config self.api = API() def login(self, username, password): + """Retrieve apikey and crypto settings from server.""" response = self.api.login(username, password) self.config.set('User', 'username', username) apikey = response['key'] @@ -21,5 +24,6 @@ def login(self, username, password): self.config.write() def logout(self): + """Remove apikey and other credentials.""" self.config.remove_section('User') self.config.write() diff --git a/serverauditor_sshconfig/app.py b/serverauditor_sshconfig/app.py index 57d85b4..e305c5a 100644 --- a/serverauditor_sshconfig/app.py +++ b/serverauditor_sshconfig/app.py @@ -1,4 +1,5 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- +"""Module for main app class.""" import logging # pylint: disable=import-error from cliff.app import App @@ -9,9 +10,11 @@ # pylint: disable=too-few-public-methods -class ServerauditorApp(object, App): +class ServerauditorApp(App): + """Class for CLI application.""" def __init__(self): + """Construct new CLI application.""" super(ServerauditorApp, self).__init__( description='Serverauditor app', version=get_version(), @@ -19,6 +22,7 @@ def __init__(self): ) def configure_logging(self): + """Change logging level for request package.""" super(ServerauditorApp, self).configure_logging() logging.getLogger('requests').setLevel(logging.WARNING) return diff --git a/serverauditor_sshconfig/cloud/__init__.py b/serverauditor_sshconfig/cloud/__init__.py index e69de29..eb25052 100644 --- a/serverauditor_sshconfig/cloud/__init__.py +++ b/serverauditor_sshconfig/cloud/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Package with user data manipulation command.""" diff --git a/serverauditor_sshconfig/cloud/commands/__init__.py b/serverauditor_sshconfig/cloud/commands/__init__.py index 457bc45..b23a934 100644 --- a/serverauditor_sshconfig/cloud/commands/__init__.py +++ b/serverauditor_sshconfig/cloud/commands/__init__.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Package with sync-cloud commands.""" from ...core.commands import AbstractCommand from .host import HostCommand, HostsCommand # noqa @@ -13,6 +15,10 @@ class InfoCommand(AbstractCommand): """Show info about host or group.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(InfoCommand, self).get_parser(prog_name) parser.add_argument( '-G', '--group', dest='entry_type', @@ -37,5 +43,6 @@ def get_parser(self, prog_name): # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" self.log.info('Info about group or host.') assert False, 'Not implemented' diff --git a/serverauditor_sshconfig/cloud/commands/group.py b/serverauditor_sshconfig/cloud/commands/group.py index 6a18f46..ff60b89 100644 --- a/serverauditor_sshconfig/cloud/commands/group.py +++ b/serverauditor_sshconfig/cloud/commands/group.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module with Group commands.""" from operator import attrgetter from ...core.commands import DetailCommand, ListCommand from ..models import Group @@ -11,10 +13,15 @@ class GroupCommand(DetailCommand): model_class = Group def __init__(self, *args, **kwargs): + """Construct new group command.""" super(GroupCommand, self).__init__(self, *args, **kwargs) self.ssh_config_args = SshConfigArgs() def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(GroupCommand, self).get_parser(prog_name) parser.add_argument( '--generate-key', action='store_true', @@ -32,10 +39,12 @@ def get_parser(self, prog_name): return parser def create(self, parsed_args): + """Handle create new instance command.""" self.create_instance(parsed_args) # pylint: disable=no-self-use def serialize_args(self, args, instance=None): + """Convert args to instance.""" if instance: ssh_config = self.ssh_config_args.serialize_args( args, instance.ssh_config @@ -57,6 +66,10 @@ class GroupsCommand(ListCommand): """Manage group objects.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(GroupsCommand, self).get_parser(prog_name) parser.add_argument( '-r', '--recursive', action='store_true', @@ -71,6 +84,7 @@ def get_parser(self, prog_name): # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" assert False, 'Filtering and recursive not implemented.' groups = self.storage.get_all(Group) fields = Group.allowed_fields() diff --git a/serverauditor_sshconfig/cloud/commands/host.py b/serverauditor_sshconfig/cloud/commands/host.py index 85bd460..ec68397 100644 --- a/serverauditor_sshconfig/cloud/commands/host.py +++ b/serverauditor_sshconfig/cloud/commands/host.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module with Host commands.""" from operator import attrgetter from ...core.exceptions import ArgumentRequiredException from ...core.commands import DetailCommand, ListCommand @@ -12,10 +14,15 @@ class HostCommand(DetailCommand): model_class = Host def __init__(self, *args, **kwargs): + """Construct new host command.""" super(HostCommand, self).__init__(self, *args, **kwargs) self.ssh_config_args = SshConfigArgs() def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(HostCommand, self).get_parser(prog_name) parser.add_argument( '--generate-key', action='store_true', @@ -42,6 +49,7 @@ def get_parser(self, prog_name): return parser def create(self, parsed_args): + """Handle create new instance command.""" if not parsed_args.address: raise ArgumentRequiredException('Address is required.') @@ -49,6 +57,7 @@ def create(self, parsed_args): # pylint: disable=no-self-use def serialize_args(self, args, instance=None): + """Convert args to instance.""" if instance: ssh_config = self.ssh_config_args.serialize_args( args, instance.ssh_config @@ -71,6 +80,10 @@ class HostsCommand(ListCommand): """Manage host objects.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(HostsCommand, self).get_parser(prog_name) parser.add_argument( '-t', '--tags', metavar='TAG_LIST', @@ -84,6 +97,7 @@ def get_parser(self, prog_name): # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" assert False, 'Filtering by tags and groups not implemented.' hosts = self.storage.get_all(Host) fields = Host.allowed_fields() diff --git a/serverauditor_sshconfig/cloud/commands/pf_rule.py b/serverauditor_sshconfig/cloud/commands/pf_rule.py index c769713..95861b9 100644 --- a/serverauditor_sshconfig/cloud/commands/pf_rule.py +++ b/serverauditor_sshconfig/cloud/commands/pf_rule.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module with PFRule commands.""" import re from operator import attrgetter from ...core.exceptions import ( @@ -17,6 +19,7 @@ class PFRuleCommand(DetailCommand): @property # pylint: disable=no-self-use def binding_parsers(self): + """Return binding parser per type abbreviation.""" return { 'D': BindingParser.dynamic, 'L': BindingParser.local, @@ -24,6 +27,10 @@ def binding_parsers(self): } def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(PFRuleCommand, self).get_parser(prog_name) parser.add_argument( '-H', '--host', metavar='HOST_ID or HOST_NAME', @@ -53,15 +60,18 @@ def get_parser(self, prog_name): return parser def parse_binding(self, pf_type, binding): + """Parse binding string to dict.""" return self.binding_parsers[pf_type](binding) def create(self, parsed_args): + """Handle create new instance command.""" if not parsed_args.host: raise ArgumentRequiredException('Host is required.') self.create_instance(parsed_args) def serialize_args(self, args, instance=None): + """Convert args to instance.""" if instance: pfrule, host = instance, instance.host else: @@ -78,6 +88,7 @@ def serialize_args(self, args, instance=None): return pfrule def get_host(self, arg): + """Retrieve host from storage.""" try: host_id = int(arg) except ValueError: @@ -96,6 +107,7 @@ class PFRulesCommand(ListCommand): # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" pf_rules = self.storage.get_all(PFRule) fields = PFRule.allowed_fields() getter = attrgetter(*fields) @@ -103,10 +115,16 @@ def take_action(self, parsed_args): class InvalidBinding(InvalidArgumentException): + """Raise it when binding can not be parsed.""" + pass class BindingParser(object): + """Binding string parser. + + Binding string is string like '[localhost:]localport:hostanme:remote port'. + """ local_pf_re = re.compile( r'^((?P[\w.]+):)?(?P\d+)' @@ -119,7 +137,7 @@ class BindingParser(object): ) @classmethod - def parse(cls, regexp, binding_str): + def _parse(cls, regexp, binding_str): matched = regexp.match(binding_str) if not matched: raise InvalidBinding('Invalid binding format.') @@ -127,9 +145,12 @@ def parse(cls, regexp, binding_str): @classmethod def local(cls, binding_str): - return cls.parse(cls.local_pf_re, binding_str) + """Parse local port forwarding binding string to dict.""" + return cls._parse(cls.local_pf_re, binding_str) remote = local + """Parse remote port forwarding binding string to dict.""" @classmethod def dynamic(cls, binding_str): - return cls.parse(cls.dynamic_pf_re, binding_str) + """Parse dynamic port forwarding binding string to dict.""" + return cls._parse(cls.dynamic_pf_re, binding_str) diff --git a/serverauditor_sshconfig/cloud/commands/snippet.py b/serverauditor_sshconfig/cloud/commands/snippet.py index f6a4556..a943731 100644 --- a/serverauditor_sshconfig/cloud/commands/snippet.py +++ b/serverauditor_sshconfig/cloud/commands/snippet.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module with Snippet commands.""" from operator import attrgetter from ...core.commands import ListCommand, DetailCommand from ...core.exceptions import ArgumentRequiredException @@ -11,6 +13,10 @@ class SnippetCommand(DetailCommand): model_class = Snippet def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(SnippetCommand, self).get_parser(prog_name) parser.add_argument( '-s', '--script', metavar='SCRIPT', @@ -23,6 +29,7 @@ def get_parser(self, prog_name): return parser def create(self, parsed_args): + """Handle create new instance command.""" if not parsed_args.script: raise ArgumentRequiredException('Script is required') @@ -30,6 +37,7 @@ def create(self, parsed_args): # pylint: disable=no-self-use def serialize_args(self, args, instance=None): + """Convert args to instance.""" if instance: snippet = instance else: @@ -45,6 +53,7 @@ class SnippetsCommand(ListCommand): # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" groups = self.storage.get_all(Snippet) fields = Snippet.allowed_fields() getter = attrgetter(*fields) diff --git a/serverauditor_sshconfig/cloud/commands/ssh_config.py b/serverauditor_sshconfig/cloud/commands/ssh_config.py index 4e60667..d920a20 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_config.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_config.py @@ -1,10 +1,14 @@ +# -*- coding: utf-8 -*- +"""Module with Sshconfig's args helper.""" from ..models import SshConfig, SshIdentity class SshConfigArgs(object): + """Class for ssh config argument adding and serializing.""" # pylint: disable=no-self-use def add_agrs(self, parser): + """Add to arg parser ssh config options.""" parser.add_argument( '-p', '--port', type=int, metavar='PORT', @@ -47,6 +51,7 @@ def add_agrs(self, parser): return parser def serialize_args(self, args, instance): + """Convert args to instance.""" if instance: ssh_identity = ( instance.ssh_identity diff --git a/serverauditor_sshconfig/cloud/commands/ssh_identity.py b/serverauditor_sshconfig/cloud/commands/ssh_identity.py index c26f40c..45aa788 100644 --- a/serverauditor_sshconfig/cloud/commands/ssh_identity.py +++ b/serverauditor_sshconfig/cloud/commands/ssh_identity.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module for ssh identity command.""" from operator import attrgetter from ...core.commands import DetailCommand, ListCommand from ..models import SshIdentity @@ -10,6 +12,10 @@ class SshIdentityCommand(DetailCommand): model_class = SshIdentity def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(SshIdentityCommand, self).get_parser(prog_name) parser.add_argument( '--generate-key', action='store_true', @@ -25,7 +31,7 @@ def get_parser(self, prog_name): ) parser.add_argument( '-i', '--identity-file', - metavar='PRIVATE_KEY', help="Private key." + metavar='PRIVATE_KEY', help='Private key.' ) parser.add_argument( '-k', '--ssh-key', @@ -38,10 +44,12 @@ def get_parser(self, prog_name): return parser def create(self, parsed_args): + """Handle create new instance command.""" self.create_instance(parsed_args) # pylint: disable=no-self-use def serialize_args(self, args, instance=None): + """Convert args to instance.""" if instance: identity = instance else: @@ -66,6 +74,7 @@ class SshIdentitiesCommand(ListCommand): # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" ssh_identities = self.storage.get_all(SshIdentity) fields = SshIdentity.allowed_fields() getter = attrgetter(*fields) diff --git a/serverauditor_sshconfig/cloud/commands/sync.py b/serverauditor_sshconfig/cloud/commands/sync.py index f3eb78e..ea2ebfd 100644 --- a/serverauditor_sshconfig/cloud/commands/sync.py +++ b/serverauditor_sshconfig/cloud/commands/sync.py @@ -1,6 +1,8 @@ -import six +# -*- coding: utf-8 -*- +"""Module for pull and push command.""" import abc from base64 import b64decode +import six from ...core.commands import AbstractCommand from ..controllers import ApiController from ..cryptor import RNCryptor @@ -9,8 +11,13 @@ @six.add_metaclass(abc.ABCMeta) class CloudSynchronizationCommand(AbstractCommand): + """Base class for pull and push commands.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(CloudSynchronizationCommand, self).get_parser(prog_name) parser.add_argument( '-s', '--strategy', metavar='STRATEGY_NAME', @@ -21,9 +28,11 @@ def get_parser(self, prog_name): @abc.abstractmethod def process_sync(self, api_controller): + """Do sync staff here.""" pass def take_action(self, parsed_args): + """Process CLI call.""" encryption_salt = b64decode(self.config.get('User', 'salt')) hmac_salt = b64decode(self.config.get('User', 'hmac_salt')) password = parsed_args.get('password', None) @@ -44,6 +53,7 @@ class PushCommand(CloudSynchronizationCommand): get_strategy = RelatedGetStrategy def process_sync(self, api_controller): + """Push outdated local instances.""" api_controller.post_bulk() self.log.info('Push data to Serverauditor cloud.') @@ -52,5 +62,6 @@ class PullCommand(CloudSynchronizationCommand): """Pull data from Serverauditor cloud.""" def process_sync(self, api_controller): + """Pull updated remote instances.""" api_controller.get_bulk() self.log.info('Pull data from Serverauditor cloud.') diff --git a/serverauditor_sshconfig/cloud/commands/tag.py b/serverauditor_sshconfig/cloud/commands/tag.py index e6d38ae..d116328 100644 --- a/serverauditor_sshconfig/cloud/commands/tag.py +++ b/serverauditor_sshconfig/cloud/commands/tag.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module for tag command.""" from ...core.commands import ListCommand @@ -5,6 +7,10 @@ class TagsCommand(ListCommand): """Manage tag objects.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(TagsCommand, self).get_parser(prog_name) parser.add_argument( '-d', '--delete', @@ -12,11 +18,12 @@ def get_parser(self, prog_name): ) parser.add_argument( 'tags', nargs='+', metavar='TAG_ID or TAG_NAME', - help="List infos about this tags." + help='List infos about this tags.' ) return parser # pylint: disable=unused-argument def take_action(self, parsed_args): + """Process CLI call.""" self.log.info('Tag objects.') assert False, 'Not implemented' diff --git a/serverauditor_sshconfig/cloud/controllers.py b/serverauditor_sshconfig/cloud/controllers.py index 0f7cdf4..3e64711 100644 --- a/serverauditor_sshconfig/cloud/controllers.py +++ b/serverauditor_sshconfig/cloud/controllers.py @@ -1,10 +1,14 @@ +# -*- coding: utf-8 -*- +"""Module for sync api controller.""" from .serializers import BulkSerializer from ..core.api import API class CryptoController(object): + """Controller for encrypting/decrypting data.""" def __init__(self, cryptor): + """Construct new crypto Controller.""" self.cryptor = cryptor # pylint: disable=no-self-use @@ -16,19 +20,23 @@ def _mutate_fields(self, model, mutator): return model def encrypt(self, model): + """Encrypt fields.""" return self._mutate_fields(model, self.cryptor.encrypt) def decrypt(self, model): + """Decrypt fields.""" return self._mutate_fields(model, self.cryptor.decrypt) class ApiController(object): + """Controller to call API.""" mapping = dict( bulk=dict(url='v2/terminal/bulk/', serializer=BulkSerializer) ) def __init__(self, storage, config, cryptor): + """Create new API controller.""" self.config = config username = self.config.get('User', 'username') apikey = self.config.get('User', 'apikey') @@ -48,6 +56,7 @@ def _get(self, mapped): return model def get_bulk(self): + """Get remote instances.""" mapped = self.mapping['bulk'] model = self._get(mapped) self.config.set('CloudSynchronization', 'last_synced', @@ -67,6 +76,7 @@ def _post(self, mapped, request_model): return response_model def post_bulk(self): + """Send local instances.""" mapped = self.mapping['bulk'] model = {} model['last_synced'] = self.config.get( diff --git a/serverauditor_sshconfig/cloud/cryptor.py b/serverauditor_sshconfig/cloud/cryptor.py index b4edb3e..ff9e5f2 100644 --- a/serverauditor_sshconfig/cloud/cryptor.py +++ b/serverauditor_sshconfig/cloud/cryptor.py @@ -1,5 +1,5 @@ -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Module with cryptor implementation.""" from __future__ import print_function import base64 @@ -13,11 +13,70 @@ from ..core.utils import bchr, bord, to_bytes, to_str -class CryptorException(Exception): - pass +class CryptoSettings(object): + """Store cryptor settings.""" + + def __init__(self): + """Construct new cryptor settings.""" + self._password = None + self._encryption_salt = None + self._hmac_salt = None + self._encryption_key = None + self._hmac_key = None + + @property + def password(self): + """Get password.""" + return self._password + + @password.setter + def password(self, value): + """Set password.""" + self._password = to_bytes(value) + + @property + def encryption_salt(self): + """Get salt for encryption key.""" + return self._encryption_salt + + @encryption_salt.setter + def encryption_salt(self, value): + """Set salt for encryption key.""" + self._encryption_salt = to_bytes(value) + @property + def hmac_salt(self): + """Get salt for hmac key.""" + return self._hmac_salt + + @hmac_salt.setter + def hmac_salt(self, value): + """Set salt for hmac key.""" + self._hmac_salt = to_bytes(value) + + @property + def initialization_vector(self): + """Generate random bytes.""" + return Random.new().read(self.AES_BLOCK_SIZE) + + @property + def encryption_key(self): + """Get encryption key.""" + if not getattr(self, '_encryption_key', None): + self._encryption_key = pbkdf2( + self.password, self.encryption_salt + ) + return self._encryption_key -class RNCryptor(object): + @property + def hmac_key(self): + """Get hmac key.""" + if not getattr(self, '_hmac_key', None): + self._hmac_key = pbkdf2(self.password, self.hmac_salt) + return self._hmac_key + + +class RNCryptor(CryptoSettings): """RNCryptor is a symmetric-key encryption schema. NB. You must set encryption_salt, hmac_salt and password @@ -28,15 +87,9 @@ class RNCryptor(object): AES_MODE = AES.MODE_CBC SALT_SIZE = 8 - def __init__(self): - self._password = None - self._encryption_salt = None - self._hmac_salt = None - self._encryption_key = None - self._hmac_key = None - # pylint: disable=no-self-use def pre_decrypt_data(self, data): + """Patch ciphertext.""" data = to_bytes(data) return base64.decodestring(data) @@ -50,6 +103,7 @@ def post_decrypt_data(self, data): return to_str(data) def decrypt(self, data): + """Decrypt data.""" data = self.pre_decrypt_data(data) length = len(data) @@ -64,13 +118,13 @@ def decrypt(self, data): if ((encryption_salt != self.encryption_salt or hmac_salt != self.hmac_salt)): - raise CryptorException("Bad encryption salt or hmac salt!") + raise CryptorException('Bad encryption salt or hmac salt!') encryption_key = self.encryption_key hmac_key = self.hmac_key if self._hmac(hmac_key, data[:length - 32]) != hmac: - raise CryptorException("Bad data!") + raise CryptorException('Bad data!') decrypted_data = self._aes_decrypt( encryption_key, initialization_vector, cipher_text @@ -87,10 +141,12 @@ def pre_encrypt_data(self, data): # pylint: disable=no-self-use def post_encrypt_data(self, data): + """Patch ciphertext.""" data = base64.encodestring(data) return to_str(data) def encrypt(self, data): + """Encrypt data.""" data = self.pre_encrypt_data(data) encryption_salt = self.encryption_salt @@ -116,48 +172,6 @@ def encrypt(self, data): return self.post_encrypt_data(encrypted_data) - @property - def password(self): - return self._password - - @password.setter - def password(self, value): - self._password = to_bytes(value) - - @property - def encryption_salt(self): - return self._encryption_salt - - @encryption_salt.setter - def encryption_salt(self, value): - self._encryption_salt = to_bytes(value) - - @property - def hmac_salt(self): - return self._hmac_salt - - @hmac_salt.setter - def hmac_salt(self, value): - self._hmac_salt = to_bytes(value) - - @property - def initialization_vector(self): - return Random.new().read(self.AES_BLOCK_SIZE) - - @property - def encryption_key(self): - if not getattr(self, '_encryption_key', None): - self._encryption_key = self._pbkdf2( - self.password, self.encryption_salt - ) - return self._encryption_key - - @property - def hmac_key(self): - if not getattr(self, '_hmac_key', None): - self._hmac_key = self._pbkdf2(self.password, self.hmac_salt) - return self._hmac_key - def _aes_encrypt(self, key, initialization_vector, text): return AES.new(key, self.AES_MODE, initialization_vector).encrypt(text) @@ -167,10 +181,19 @@ def _aes_decrypt(self, key, initialization_vector, text): def _hmac(self, key, data): return python_hmac.new(key, data, hashlib.sha256).digest() - def _pbkdf2(self, password, salt, iterations=10000, key_length=32): - return KDF.PBKDF2(password, salt, dkLen=key_length, count=iterations, - prf=preudorandom) + +class CryptorException(Exception): + """Raise cryptor face some problem with encrypting and decrypting.""" + + pass + + +def pbkdf2(password, salt, iterations=10000, key_length=32): + """Generate key.""" + return KDF.PBKDF2(password, salt, dkLen=key_length, count=iterations, + prf=preudorandom) def preudorandom(password, salt): + """Generate random for password and salt.""" return python_hmac.new(password, salt, hashlib.sha1).digest() diff --git a/serverauditor_sshconfig/cloud/models.py b/serverauditor_sshconfig/cloud/models.py index 133c7ee..1892538 100644 --- a/serverauditor_sshconfig/cloud/models.py +++ b/serverauditor_sshconfig/cloud/models.py @@ -1,7 +1,10 @@ +# -*- coding: utf-8 -*- +"""Module with user data models.""" from ..core.models import Model, Field class Tag(Model): + """Model for tag.""" fields = { 'label': Field(str, False, '') @@ -11,6 +14,7 @@ class Tag(Model): class Snippet(Model): + """Model for snippet.""" fields = { 'label': Field(str, False, ''), @@ -21,6 +25,7 @@ class Snippet(Model): class SshKey(Model): + """Model for ssh key.""" fields = { 'label': Field(str, False, ''), @@ -33,6 +38,7 @@ class SshKey(Model): class SshIdentity(Model): + """Model for ssh identity.""" fields = { 'label': Field(str, False, ''), @@ -46,6 +52,7 @@ class SshIdentity(Model): class SshConfig(Model): + """Model for ssh config.""" fields = { 'port': Field(int, False, None), @@ -56,6 +63,7 @@ class SshConfig(Model): class Group(Model): + """Model for group.""" fields = { 'label': Field(str, False, ''), @@ -69,6 +77,7 @@ class Group(Model): class Host(Model): + """Model for host.""" fields = { 'label': Field(str, False, ''), @@ -81,6 +90,7 @@ class Host(Model): class TagHost(Model): + """Model for relation host and tags.""" fields = { 'host': Field(Host, False, None), @@ -90,6 +100,7 @@ class TagHost(Model): class PFRule(Model): + """Model for port forwarding.""" fields = { 'label': Field(str, False, ''), diff --git a/serverauditor_sshconfig/cloud/serializers.py b/serverauditor_sshconfig/cloud/serializers.py index 1806ca5..865c9bb 100644 --- a/serverauditor_sshconfig/cloud/serializers.py +++ b/serverauditor_sshconfig/cloud/serializers.py @@ -1,9 +1,9 @@ +# -*- coding: utf-8 -*- """Serializers (read controllers) is like django rest framework serializers.""" - -import six import abc from collections import OrderedDict, defaultdict from operator import attrgetter, itemgetter +import six from ..core.models import RemoteInstance from ..core.exceptions import DoesNotExistException from ..core.storage.strategies import SoftDeleteStrategy @@ -18,14 +18,17 @@ def map_zip_model_fields(model, field_getter=None): + """Return list of tuples (field_object, field_value).""" field_getter = field_getter or attrgetter(model.fields) return zip(model.fields, field_getter(model)) @six.add_metaclass(abc.ABCMeta) class Serializer(object): + """Base serializer.""" def __init__(self, storage): + """Create new Serializer.""" assert storage self.storage = storage @@ -41,23 +44,28 @@ def to_payload(self, model): # pylint: disable=abstract-method class BulkEntryBaseSerializer(Serializer): + """Base serializer for one model.""" def __init__(self, model_class, **kwargs): + """Create new entry serializer.""" super(BulkEntryBaseSerializer, self).__init__(**kwargs) assert model_class self.model_class = model_class class BulkPrimaryKeySerializer(BulkEntryBaseSerializer): + """Serializer for primary key payloads.""" to_model_mapping = defaultdict( lambda: ID_GETTER, {int: int, } ) def id_from_payload(self, payload): + """Get remote id from payload.""" return self.to_model_mapping[type(payload)](payload) def to_model(self, payload): + """Retrieve model from storage by payload.""" if not payload: return None @@ -69,6 +77,7 @@ def to_model(self, payload): return model def to_payload(self, model): + """Convert model to primary key or to set/id reference.""" if not model: return None if model.remote_instance: @@ -79,8 +88,10 @@ def to_payload(self, model): # pylint: disable=too-few-public-methods class GetPrimaryKeySerializerMixin(object): + """Mixin to get primary get serializer.""" def get_primary_key_serializer(self, model_class): + """Create new primary key serializer.""" return BulkPrimaryKeySerializer( storage=self.storage, model_class=model_class ) @@ -88,13 +99,16 @@ def get_primary_key_serializer(self, model_class): class BulkEntrySerializer(GetPrimaryKeySerializerMixin, BulkPrimaryKeySerializer): + """Serializer for complete model.""" def __init__(self, **kwargs): + """Create new serializer.""" super(BulkEntrySerializer, self).__init__(**kwargs) self.attrgetter = attrgetter(*self.model_class.fields) self.remote_instance_attrgetter = attrgetter(*RemoteInstance.fields) def to_payload(self, model): + """Convert model to payload.""" payload = dict(map_zip_model_fields(model, self.attrgetter)) if model.remote_instance: zipped_remote_instance = map_zip_model_fields( @@ -109,16 +123,19 @@ def to_payload(self, model): return payload def serialize_related_field(self, model, field, mapping): + """Serializer relation to payload.""" related_serializer = self.get_primary_key_serializer(mapping.model) fk_payload = related_serializer.to_payload(getattr(model, field)) return fk_payload def to_model(self, payload): + """Convert payload to model.""" model = self.get_or_initialize_model(payload) model = self.update_model_fields(model, payload) return model def update_model_fields(self, model, payload): + """Update model's fields with payload.""" fk_fields = model.fk_field_names() for i in model.fields: if i in fk_fields: @@ -131,6 +148,7 @@ def update_model_fields(self, model, payload): return model def get_or_initialize_model(self, payload): + """Get existed model or generate new one using payload.""" try: model = self.get_model(payload) except DoesNotExistException: @@ -140,9 +158,11 @@ def get_or_initialize_model(self, payload): return model def get_model(self, payload): + """Get model for payload.""" return super(BulkEntrySerializer, self).to_model(payload) def initialize_model(self, payload): + """Generate new model using payload.""" remote_instance = self.create_remote_instance(payload) model = self.model_class() model.remote_instance = remote_instance @@ -153,6 +173,7 @@ def initialize_model(self, payload): # pylint: disable=no-self-use def create_remote_instance(self, payload): + """Generate remote instance for payload.""" remote_instance = RemoteInstance() for i, field in RemoteInstance.fields.items(): setattr(remote_instance, i, payload.pop(i, field.default)) @@ -160,22 +181,27 @@ def create_remote_instance(self, payload): class CryptoBulkEntrySerializer(BulkEntrySerializer): + """Entry serializer that encrypt model and decrypt payload.""" def __init__(self, crypto_controller, **kwargs): + """Construct new crypto serializer for bulk entry.""" super(CryptoBulkEntrySerializer, self).__init__(**kwargs) self.crypto_controller = crypto_controller def to_model(self, payload): + """Decrypt model after serialization.""" model = super(CryptoBulkEntrySerializer, self).to_model(payload) return self.crypto_controller.decrypt(model) def to_payload(self, model): + """Encrypt model before deserialization.""" encrypted_model = self.crypto_controller.encrypt(model) return super(CryptoBulkEntrySerializer, self).to_payload( encrypted_model) class BulkSerializer(GetPrimaryKeySerializerMixin, Serializer): + """Serializer for entry list.""" child_serializer_class = CryptoBulkEntrySerializer supported_models = ( @@ -183,6 +209,7 @@ class BulkSerializer(GetPrimaryKeySerializerMixin, Serializer): ) def __init__(self, crypto_controller, **kwargs): + """Construct new serializer for entry list.""" super(BulkSerializer, self).__init__(**kwargs) self.crypto_controller = crypto_controller self.mapping = OrderedDict(( @@ -191,6 +218,7 @@ def __init__(self, crypto_controller, **kwargs): )) def to_model(self, payload): + """Convert payload with set list.""" models = {} models['last_synced'] = payload.pop('now') models['deleted_sets'] = {} @@ -217,6 +245,7 @@ def to_model(self, payload): return models def to_payload(self, model): + """Convert model to payload with set list.""" payload = {} payload['last_synced'] = model.pop('last_synced') payload['delete_sets'] = self.get_delete_strategy().get_delete_sets() @@ -234,15 +263,18 @@ def to_payload(self, model): return payload def get_delete_strategy(self): + """Create delete strategy.""" return SoftDeleteStrategy(self.storage) def create_child_serializer(self, model_class): + """Generate specific set serializer.""" return self.child_serializer_class( model_class=model_class, storage=self.storage, crypto_controller=self.crypto_controller ) def process_model_entries(self, updated, deleted): + """Handle updated and deleted models.""" for i in updated: self.storage.save(i) for i in deleted: diff --git a/serverauditor_sshconfig/core/__init__.py b/serverauditor_sshconfig/core/__init__.py index e69de29..a92211c 100644 --- a/serverauditor_sshconfig/core/__init__.py +++ b/serverauditor_sshconfig/core/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Package with core features.""" diff --git a/serverauditor_sshconfig/core/api.py b/serverauditor_sshconfig/core/api.py index e551835..8f36e1a 100644 --- a/serverauditor_sshconfig/core/api.py +++ b/serverauditor_sshconfig/core/api.py @@ -1,67 +1,78 @@ -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Package with api client.""" import logging -import six import hashlib +import six import requests from requests.auth import AuthBase # pylint: disable=too-few-public-methods class ServerauditorAuth(AuthBase): + """Authentication method to sync-cloud.""" header_name = 'Authorization' def __init__(self, username, apikey): + """Create new authenticator.""" self.username = username self.apikey = apikey @property def auth_header(self): - return "ApiKey {username}:{apikey}".format( + """Render auth header content.""" + return 'ApiKey {username}:{apikey}'.format( username=self.username, apikey=self.apikey ) def __call__(self, request): + """Add header to request.""" request.headers[self.header_name] = self.auth_header return request def __str__(self): + """Convert it to string.""" return unicode(self).encode('utf-8') def __unicode__(self): + """Convert it to string.""" return '{key}: {value}'.format( key=self.header_name, value=self.auth_header ) def hash_password(password): + """Generate hash from password.""" password = six.b(password) return hashlib.sha256(password).hexdigest() class API(object): + """Class to send requests to sync cloud.""" host = 'serverauditor.com' base_url = 'https://{}/api/'.format(host) logger = logging.getLogger(__name__) def __init__(self, username=None, apikey=None): + """Construct new API instance.""" if username and apikey: self.auth = ServerauditorAuth(username, apikey) else: self.auth = None def set_auth(self, username, apikey): + """Provide credentials.""" self.auth = ServerauditorAuth(username, apikey) def request_url(self, endpoint): + """Create full url to endpoint.""" return self.base_url + endpoint def login(self, username, password): """Return user's auth token.""" password = hash_password(password) - response = requests.get(self.request_url("v1/token/auth/"), + response = requests.get(self.request_url('v1/token/auth/'), auth=(username, password)) assert response.status_code == 200 @@ -71,22 +82,26 @@ def login(self, username, password): return response_payload def post(self, endpoint, data): + """Send authorized post request.""" response = requests.post(self.request_url(endpoint), json=data, auth=self.auth) assert response.status_code == 201 return response.json() def get(self, endpoint): + """Send authorized get request.""" response = requests.get(self.request_url(endpoint), auth=self.auth) assert response.status_code == 200 return response.json() def delete(self, endpoint): + """Send authorized delete request.""" response = requests.delete(self.request_url(endpoint), auth=self.auth) assert response.status_code in (200, 204) return response.json() def put(self, endpoint, data): + """Send authorized put request.""" response = requests.put(self.request_url(endpoint), json=data, auth=self.auth) assert response.status_code in (200, 204) diff --git a/serverauditor_sshconfig/core/commands.py b/serverauditor_sshconfig/core/commands.py index 31b65e9..81c6585 100644 --- a/serverauditor_sshconfig/core/commands.py +++ b/serverauditor_sshconfig/core/commands.py @@ -1,4 +1,5 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- +"""Module with base CLI commands.""" import logging import getpass @@ -21,12 +22,16 @@ # pylint: disable=too-few-public-methods class PasswordPromptMixin(object): + """Mixin to command to call account password prompt.""" + # pylint: disable=no-self-use def prompt_password(self): + """Ask user to enter password in secure way.""" return getpass.getpass("Serverauditor's password:") -class AbstractCommand(object, PasswordPromptMixin, Command): +# pylint: disable=abstract-method +class AbstractCommand(PasswordPromptMixin, Command): """Abstract Command with log.""" log = logging.getLogger(__name__) @@ -36,6 +41,7 @@ class AbstractCommand(object, PasswordPromptMixin, Command): delete_strategy = DeleteStrategy def __init__(self, app, app_args, cmd_name=None): + """Construct new command.""" super(AbstractCommand, self).__init__(app, app_args, cmd_name) self.config = Config(self.app.NAME) self.storage = ApplicationStorage( @@ -45,12 +51,17 @@ def __init__(self, app, app_args, cmd_name=None): ) def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(AbstractCommand, self).get_parser(prog_name) - parser.add_argument('--log-file', help="Path to log file.") + parser.add_argument('--log-file', help='Path to log file.') return parser class DetailCommand(AbstractCommand): + """Command for operating with models by id or names.""" save_strategy = RelatedSaveStrategy get_strategy = RelatedGetStrategy @@ -64,10 +75,15 @@ class DetailCommand(AbstractCommand): """ def __init__(self, *args, **kwargs): + """Construct new detail command.""" super(DetailCommand, self).__init__(*args, **kwargs) assert self.all_operations.intersection(self.allowed_operations) def update(self, parsed_args): + """Handle update command. + + Get models from storage, parse args and update models. + """ if not parsed_args.entry: raise ArgumentRequiredException( 'At least one ID or NAME are required.' @@ -77,6 +93,10 @@ def update(self, parsed_args): self.update_instance(parsed_args, i) def delete(self, parsed_args): + """Handle delete command. + + Get models from storage, delete models. + """ if not parsed_args.entry: raise ArgumentRequiredException( 'At least one ID or NAME are required.' @@ -87,17 +107,24 @@ def delete(self, parsed_args): @property def is_allow_delete(self): + """Check is command handle model deleting.""" return 'delete' in self.allowed_operations @property def is_allow_update(self): + """Check is command handle model updating.""" return 'update' in self.allowed_operations @property def is_allow_create(self): + """Check is command handle model creating.""" return 'create' in self.allowed_operations def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(DetailCommand, self).get_parser(prog_name) if self.is_allow_delete: parser.add_argument( @@ -121,31 +148,36 @@ def get_parser(self, prog_name): return parser - def take_edit(self, parsed_args): - if self.is_allow_create and not parsed_args.entry: - self.create(parsed_args) - elif self.is_allow_update and parsed_args.entry: - self.update(parsed_args) - def take_action(self, parsed_args): + """Process CLI call.""" if self.is_allow_delete and parsed_args.delete: self.delete(parsed_args) else: - self.take_edit(parsed_args) + if self.is_allow_create and not parsed_args.entry: + self.create(parsed_args) + elif self.is_allow_update and parsed_args.entry: + self.update(parsed_args) def log_create(self, entry): + """Log creating new model entry.""" self.app.stdout.write('{}\n'.format(entry.id)) self.log.info('Create object.') def log_update(self, entry): + """Log updating model entry.""" self.app.stdout.write('{}\n'.format(entry.id)) self.log.info('Update object.') def log_delete(self, entry): + """Log deleting model entry.""" self.app.stdout.write('{}\n'.format(entry.id)) self.log.info('Delete object.') def get_objects(self, ids__names): + """Get model list. + + Models will match id and label with passed ids__names list. + """ ids, names = self.parse_ids_names(ids__names) instances = self.storage.filter( self.model_class, any, @@ -157,38 +189,48 @@ def get_objects(self, ids__names): # pylint: disable=no-self-use def parse_ids_names(self, ids__names): + """Parse ids__models list.""" ids = [int(i) for i in ids__names if i.isdigit()] return ids, ids__names def create_instance(self, args): + """Create new model entry.""" instance = self.serialize_args(args) with self.storage: saved_instance = self.storage.save(instance) self.log_create(saved_instance) def update_instance(self, args, instance): + """Update model entry.""" updated_instance = self.serialize_args(args, instance) with self.storage: self.storage.save(updated_instance) self.log_update(updated_instance) def delete_instance(self, instance): + """Delete model entry.""" with self.storage: self.storage.delete(instance) self.log_delete(instance) -# pylint: disable=too-few-public-methods -class ListCommand(object, Lister): +# pylint: disable=too-few-public-methods, abstract-method +class ListCommand(Lister): + """Command for listing storage content.""" log = logging.getLogger(__name__) def __init__(self, app, app_args, cmd_name=None): + """Create new command instance.""" super(ListCommand, self).__init__(app, app_args, cmd_name) self.config = Config(self.app.NAME) self.storage = ApplicationStorage(self.app.NAME) def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(ListCommand, self).get_parser(prog_name) - parser.add_argument('--log-file', help="Path to log file.") + parser.add_argument('--log-file', help='Path to log file.') return parser diff --git a/serverauditor_sshconfig/core/exceptions.py b/serverauditor_sshconfig/core/exceptions.py index 083d981..fda43bc 100644 --- a/serverauditor_sshconfig/core/exceptions.py +++ b/serverauditor_sshconfig/core/exceptions.py @@ -1,14 +1,26 @@ +# -*- coding: utf-8 -*- +"""Module for application exceptions.""" + + class DoesNotExistException(Exception): + """Raise it when model can not be found in storage.""" + pass class TooManyEntriesException(Exception): + """Raise it when there are more models then you think.""" + pass class ArgumentRequiredException(ValueError): + """Raise it when one of required CLI argument is missed.""" + pass class InvalidArgumentException(ValueError): + """Raise it when CLI argument have invalid value.""" + pass diff --git a/serverauditor_sshconfig/core/models.py b/serverauditor_sshconfig/core/models.py index a0e376a..49b2790 100644 --- a/serverauditor_sshconfig/core/models.py +++ b/serverauditor_sshconfig/core/models.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module with general model implementation.""" import copy from collections import namedtuple @@ -6,6 +8,7 @@ class AbstractModel(dict): + """Base class for all models.""" fields = dict() _mandatory_fields = dict() @@ -19,6 +22,7 @@ def _fields(cls): @classmethod def allowed_fields(cls): + """Return list of fields for application usage.""" return cls._fields().keys() @classmethod @@ -27,32 +31,39 @@ def _validate_attr(cls, name): raise AttributeError def __getattr__(self, name): + """Get field from self.""" self._validate_attr(name) default = self._fields()[name].default return self.get(name, default) def __setattr__(self, name, value): + """Set field to self.""" self._validate_attr(name) self[name] = value def __delattr__(self, name): + """Delete key from self.""" self._validate_attr(name) del self[name] def copy(self): + """Wrap dict.copy into model.""" return self.__copy__() def __copy__(self): + """Wrap dict.copy into model.""" newone = type(self)() newone.update(self) return newone # pylint: disable=unused-argument def __deepcopy__(self, requesteddeepcopy): + """Wrap dict.deepcopy into model.""" return type(self)(copy.deepcopy(super(AbstractModel, self))) class RemoteInstance(AbstractModel): + """Class that represent model sync revision.""" fields = { 'id': Field(long, False, None), @@ -63,6 +74,7 @@ class RemoteInstance(AbstractModel): class Model(AbstractModel): + """Base model with relations.""" _mandatory_fields = { 'id': Field(long, False, None), @@ -70,6 +82,7 @@ class Model(AbstractModel): } def __init__(self, *args, **kwargs): + """Create new model and patch remote instance.""" # The simplest way to make lint not raise # access-member-before-definition self.remote_instance = None @@ -83,16 +96,19 @@ def __init__(self, *args, **kwargs): @classmethod def fk_field_names(cls): + """Return name list for relation fields.""" return tuple( k for k, v in cls._fields().items() if issubclass(v.model, Model) ) def mark_updated(self): + """Mark revision as outdated.""" if self.remote_instance: # pylint: disable=attribute-defined-outside-init self.remote_instance.state = 'updated' def mark_synced(self): + """Mark revision as updated.""" if self.remote_instance: # pylint: disable=attribute-defined-outside-init self.remote_instance.state = 'synced' @@ -100,13 +116,14 @@ def mark_synced(self): # set_name = '' # """Key name in Application Storage.""" crypto_fields = {} - """Set of fields for enrpyption and decryption on cloud.""" + """Set of fields for encryption and decryption on cloud.""" id_name = 'id' """Name of field to be used as identificator.""" class DeleteSets(AbstractModel): + """Class to keep deleted model remote references.""" fields = { 'tag_set': Field(list, False, None), @@ -123,13 +140,15 @@ class DeleteSets(AbstractModel): default_field_value = list def update_field(self, field, set_operator): + """Update the one of deleted_set.""" existed = getattr(self, field) or self.default_field_value() existed_set = set(existed) new_set = set_operator(existed_set) new = self.default_field_value(new_set) setattr(self, self.set_name, new) - def soft_delete(self, model): + def store(self, model): + """Add model id to deleted_sets.""" if not model.remote_instance: return @@ -138,7 +157,8 @@ def union(existed_set): self.update_field(model.set_name, union) - def delete_soft_deleted(self, set_name, identity): + def remove(self, set_name, identity): + """Remove id from deleted_sets.""" if not identity: return diff --git a/serverauditor_sshconfig/core/settings.py b/serverauditor_sshconfig/core/settings.py index 19bf23c..b4c9feb 100644 --- a/serverauditor_sshconfig/core/settings.py +++ b/serverauditor_sshconfig/core/settings.py @@ -1,15 +1,17 @@ -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Module for keeping application config.""" import os import six from .utils import expand_and_format_path class Config(object): + """Class for application config.""" paths = ['~/.{application_name}'] def __init__(self, application_name, **kwargs): + """Create new config.""" assert self.paths, "It must have at least single config file's path." self._paths = expand_and_format_path( self.paths, application_name=application_name, **kwargs @@ -20,30 +22,37 @@ def __init__(self, application_name, **kwargs): @property def user_config_path(self): + """Return particular user config path.""" return self._paths[-1] def touch_files(self): + """Touch config file paths.""" for i in self._paths: if not os.path.exists(i): with open(i, 'w+'): pass def get(self, *args, **kwargs): + """Get option value from config.""" return self.config.get(*args, **kwargs) def set(self, section, option, value): + """Set option value to config.""" if not self.config.has_section(section): self.config.add_section(section) self.config.set(section, option, value) def remove(self, section, option): + """Remove option value from config.""" if self.config.has_section(section): self.config.remove_option(section, option) def remove_section(self, section): + """Remove section and all options from config.""" if self.config.has_section(section): self.config.remove_section(section) def write(self): + """Write config for current user config file.""" with open(self.user_config_path, 'w') as _file: self.config.write(_file) diff --git a/serverauditor_sshconfig/core/storage/__init__.py b/serverauditor_sshconfig/core/storage/__init__.py index bd9f6f4..4abdd0f 100644 --- a/serverauditor_sshconfig/core/storage/__init__.py +++ b/serverauditor_sshconfig/core/storage/__init__.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Module for Application storage.""" from collections import namedtuple from .idgenerators import UUIDGenerator from .driver import PersistentDict @@ -9,18 +11,26 @@ # pylint: disable=too-few-public-methods class InternalModelContructor(object): + """Serializer raw data from storage to model. + + For internal use only. + """ def __init__(self, strategy): + """Create new constructor.""" self.strategy = strategy def __call__(self, raw_data, model_class): + """Return barely wrapping raw_data with model_class.""" return model_class(raw_data) # pylint: disable=too-few-public-methods class ModelContructor(InternalModelContructor): + """Serializer raw data from storage to model.""" def __call__(self, raw_data, model_class): + """Call strategy to retrieve complete model tree.""" model = super(ModelContructor, self).__call__(raw_data, model_class) return self.strategy.get(model) @@ -29,12 +39,14 @@ def __call__(self, raw_data, model_class): class ApplicationStorage(object): + """Storage for user data.""" path = '~/.{application_name}.storage' defaultstorage = list def __init__(self, application_name, save_strategy=None, get_strategy=None, delete_strategy=None, **kwargs): + """Create new storage for application.""" self._path = expand_and_format_path( [self.path], application_name=application_name, **kwargs )[0] @@ -52,17 +64,12 @@ def __init__(self, application_name, save_strategy=None, self.model_constructor = ModelContructor( self.strategies.getter) - def make_strategy(self, strategy_class, default): - strategy_class = strategy_class or default - return strategy_class(self) - - def generate_id(self, model): - return self.id_generator(model) - def __enter__(self): + """Start transaction.""" return self def __exit__(self, exc_type, exc_val, exc_tb): + """Process transaction closing and sync driver.""" self.driver.sync() def save(self, model): @@ -79,11 +86,13 @@ def save(self, model): return saved_model def create(self, model): + """Add new model in it's list.""" assert not getattr(model, model.id_name) model.id = self.generate_id(model) return self._internal_update(model) def update(self, model): + """Update existed model in it's list.""" identificator = getattr(model, model.id_name) assert identificator @@ -92,14 +101,20 @@ def update(self, model): return self._internal_update(model) def delete(self, model): + """Delete model from it's list.""" self._internal_delete(model) self.strategies.deleter.delete(model) def confirm_delete(self, deleted_sets): - # FIXME It needs more suitable name + """Remove intersection with deleted_sets from storage.""" self.strategies.deleter.confirm_delete(deleted_sets) def get(self, model_class, query_union=None, **kwargs): + """Get single model with passed lookups. + + Usage: + list = storage.get(Model, any, **{'field.ge': 1, 'field.le': 5} + """ founded_models = self.filter(model_class, query_union, **kwargs) if not founded_models: raise DoesNotExistException @@ -108,6 +123,11 @@ def get(self, model_class, query_union=None, **kwargs): return founded_models[0] def filter(self, model_class, query_union=None, **kwargs): + """Filter model list with passed lookups. + + Usage: + list = storage.filter(Model, any, **{'field.ge': 1, 'field.le': 5} + """ assert isinstance(model_class, type) assert kwargs query = Query(query_union, **kwargs) @@ -116,6 +136,7 @@ def filter(self, model_class, query_union=None, **kwargs): return founded_models def get_all(self, model_class): + """Retrieve full model list.""" return self._get_all_base(model_class, self.model_constructor) def _internal_get_all(self, model_class): @@ -148,7 +169,18 @@ def _internal_delete(self, model): self.driver[model.set_name] = models def low_get(self, key): + """Get data directly from driver.""" return self.driver[key] def low_set(self, key, value): + """Set data directly to driver.""" self.driver[key] = value + + def generate_id(self, model): + """Generate new local id.""" + return self.id_generator(model) + + def make_strategy(self, strategy_class, default): + """Create new strategy.""" + strategy_class = strategy_class or default + return strategy_class(self) diff --git a/serverauditor_sshconfig/core/storage/driver.py b/serverauditor_sshconfig/core/storage/driver.py index 804fdc5..0f02cb7 100644 --- a/serverauditor_sshconfig/core/storage/driver.py +++ b/serverauditor_sshconfig/core/storage/driver.py @@ -1,16 +1,22 @@ +# -*- coding: utf-8 -*- +"""Module for low level of application storage. + +Driver means "dict converter to stream". +""" import abc -import six +import os +from collections import OrderedDict + import pickle import json import csv -import os import shutil - -from collections import OrderedDict +import six @six.add_metaclass(abc.ABCMeta) class Driver(object): + """Base class for any storage driver.""" @abc.abstractmethod def dump(self, stream, obj_data): @@ -23,31 +29,40 @@ def load(self, stream): class PickleDriver(Driver): + """Pickle driver for dict.""" def dump(self, stream, obj_data): + """Dump obj_data to stream.""" pickle.dump(dict(obj_data), stream, 2) def load(self, stream): + """Load obj_data from stream.""" super(PickleDriver, self).load(stream) return pickle.load(stream) class JSONDriver(Driver): + """JSON driver for dict.""" def dump(self, stream, obj_data): + """Dump obj_data to stream.""" json.dump(obj_data, stream, separators=(',', ':')) def load(self, stream): + """Load obj_data from stream.""" super(JSONDriver, self).load(stream) return json.load(stream) class CSVDriver(Driver): + """CSV driver for dict.""" def dump(self, stream, obj_data): + """Dump obj_data to stream.""" csv.writer(stream).writerows(obj_data.items()) def load(self, stream): + """Load obj_data from stream.""" super(CSVDriver, self).load(stream) return csv.reader(stream) @@ -74,9 +89,16 @@ class PersistentDict(OrderedDict): def __init__(self, filename, flag='c', mode=None, _format='json', *args, **kwds): - self.flag = flag # r=readonly, c=create, or n=new - self.mode = mode # None or an octal triple like 0644 - self._format = _format # 'csv', 'json', or 'pickle' + """Construct new dict. + + :param str flag: one of ('readonly', 'create', 'new') + :param mode: file mode for result file, + None or an octal triple like 0644 + :oaram _format: one of ('csv', 'json', 'pickle') + """ + self.flag = flag + self.mode = mode + self._format = _format self.filename = filename super(PersistentDict, self).__init__(*args, **kwds) if flag != 'n' and os.access(filename, os.R_OK): @@ -111,21 +133,26 @@ def move_file_and_set_mode(src, dst, dst_mode): move_file_and_set_mode(tempname, self.filename, self.mode) def close(self): + """Close storage.""" self.sync() def __enter__(self): + """Process entering transaction.""" return self def __exit__(self, *exc_info): + """Process exiting transaction.""" self.close() def dump(self, fileobj): + """Write self to fileobj.""" try: DRIVERS[self._format].dump(fileobj, self) except KeyError: raise NotImplementedError('Unknown format: ' + repr(self._format)) def load(self, fileobj): + """Populate self with fileobj content.""" for loader in DRIVERS.values(): try: return self.update(loader.load(fileobj)) diff --git a/serverauditor_sshconfig/core/storage/idgenerators.py b/serverauditor_sshconfig/core/storage/idgenerators.py index f785196..98e31aa 100644 --- a/serverauditor_sshconfig/core/storage/idgenerators.py +++ b/serverauditor_sshconfig/core/storage/idgenerators.py @@ -1,8 +1,11 @@ +# -*- coding: utf-8 -*- +""""Package to keep storage id generating logic.""" from uuid import uuid4 # pylint: disable=too-few-public-methods class UUIDGenerator(object): + """ID generator based on uuid4.""" def __init__(self, storage): """Contruct IDGenerator. diff --git a/serverauditor_sshconfig/core/storage/operators.py b/serverauditor_sshconfig/core/storage/operators.py index 8fc6e98..4c26d9d 100644 --- a/serverauditor_sshconfig/core/storage/operators.py +++ b/serverauditor_sshconfig/core/storage/operators.py @@ -1,6 +1,11 @@ - +# -*- coding: utf-8 -*- +"""Module to keep operators that using to filter model list.""" from operator import * # noqa def rcontains(obj, seq): + """Act like contains, but takes reverse argument order. + + Motivation: use it like ge, le, gt, lt etc. (aka operator(value, const)) + """ return contains(seq, obj) diff --git a/serverauditor_sshconfig/core/storage/query.py b/serverauditor_sshconfig/core/storage/query.py index 6a751c1..4741962 100644 --- a/serverauditor_sshconfig/core/storage/query.py +++ b/serverauditor_sshconfig/core/storage/query.py @@ -1,12 +1,16 @@ +# -*- coding: utf-8 -*- +"""Package with Query classes.""" from . import operators # pylint: disable=too-few-public-methods class QueryOperator(object): + """Operators for list's filtering.""" operators = ['eq', 'ne', 'gt', 'lt', 'le', 'ge', 'rcontains', 'contains'] def __init__(self, field, value): + """Construct new operator.""" splited_field = field.split('.') operator_name = splited_field[-1] if operator_name not in self.operators: @@ -19,6 +23,7 @@ def __init__(self, field, value): self.value = value def __call__(self, obj): + """Filter single object.""" try: field = self.get_field(obj) except AttributeError: @@ -28,14 +33,17 @@ def __call__(self, obj): # pylint: disable=too-few-public-methods class Query(object): + """Query construction class (aka set of operators).""" def __init__(self, union=None, **kwargs): + """Construct new query.""" self.operators_union = union or all self.operators = [ QueryOperator(k, v) for k, v in kwargs.items() ] def __call__(self, obj): + """Call all operators for object and union results.""" filters = [i(obj) for i in self.operators] result = self.operators_union(filters) return result diff --git a/serverauditor_sshconfig/core/storage/strategies.py b/serverauditor_sshconfig/core/storage/strategies.py index 397edb9..6072be0 100644 --- a/serverauditor_sshconfig/core/storage/strategies.py +++ b/serverauditor_sshconfig/core/storage/strategies.py @@ -1,52 +1,77 @@ +# -*- coding: utf-8 -*- +"""Module with strategies. + +Strategy is some additional behaviour for storage. +""" import six from ..models import DeleteSets # pylint: disable=too-few-public-methods class Strategy(object): + """Base class for any strategy.""" def __init__(self, storage): + """Construct strategy, keep assigned storage.""" self.storage = storage class SaveStrategy(Strategy): + """Saver strategy that saves relations on storage.""" # pylint: disable=no-self-use,unused-argument def save_submodel(self, submodel, mapping): + """Save submodels of model.""" if isinstance(submodel, six.integer_types): return submodel assert submodel.id return submodel.id - def save_field(self, field, mapping): + def serialize_relation(self, field, mapping): + """Transform relation field. + + Use it to save relations. + """ return field and self.save_submodel(field, mapping) def save(self, model): + """Do extra action when model saved. + + Save it's relations. + """ model_copy = model.copy() fk_fields = model.fk_field_names() for field in fk_fields: mapping = model.fields[field] - saved_submodel = self.save_field(getattr(model, field), mapping) + saved_submodel = self.serialize_relation( + getattr(model, field), mapping + ) setattr(model_copy, field, saved_submodel) return model_copy class RelatedSaveStrategy(SaveStrategy): + """Saver strategy that does not save relations.""" def save_submodel(self, submodel, mapping): + """Do not save any, barely return relation id.""" return self.storage.save(submodel).id class GetStrategy(Strategy): + """Getter strategy that simply get model.""" # pylint: disable=no-self-use def get(self, model): + """Return what it gets.""" return model class RelatedGetStrategy(GetStrategy): + """Getter strategy that get model's relation tree.""" def get(self, model): + """Return model with whole relation tree.""" result = super(RelatedGetStrategy, self).get(model) fk_fields = model.fk_field_names() for field in fk_fields: @@ -59,24 +84,33 @@ def get(self, model): class DeleteStrategy(Strategy): + """Deleter strategy that completely delete model.""" # pylint: disable=no-self-use def get_delete_sets(self): + """Return delete objects.""" return {} # pylint: disable=no-self-use def delete(self, model): + """Return what it gets.""" return model def confirm_delete(self, deleted_sets): + """Confirm delete (Need to create more suitable description).""" pass class SoftDeleteStrategy(DeleteStrategy): + """Deleter strategy that delete model and do small extra action. + + Extra action is that store model remote id in deleted_sets if any. + """ delete_sets_class = DeleteSets def get_delete_sets(self): + """Retrieve delete objects from storage.""" try: data = self.storage.low_get(self.delete_sets_class.set_name) except KeyError: @@ -85,18 +119,20 @@ def get_delete_sets(self): return model def set_delete_sets(self, deleted): + """Store delete sets into the storage.""" self.storage.low_set(self.delete_sets_class.set_name, deleted) def delete(self, model): + """Store remote_id of model in delete sets.""" delete_sets = self.get_delete_sets() - delete_sets.soft_delete(model) + delete_sets.store(model) self.set_delete_sets(delete_sets) return model - def confirm_delete(self, sets): - # FIXME It needs more suitable name + def remove_intersection(self, sets): + """Remove from deleted_sets intersection with sets passed.""" delete_sets = self.get_delete_sets() - for key, value in sets.items(): - for i in value: - delete_sets.delete_soft_deleted(key, i) + for set_name, id_list in sets.items(): + for i in id_list: + delete_sets.remove(set_name, i) self.set_delete_sets(delete_sets) diff --git a/serverauditor_sshconfig/core/utils.py b/serverauditor_sshconfig/core/utils.py index f1e15df..1d242ba 100644 --- a/serverauditor_sshconfig/core/utils.py +++ b/serverauditor_sshconfig/core/utils.py @@ -1,7 +1,8 @@ -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Miscellaneous extra functions.""" import os def expand_and_format_path(paths, **kwargs): + """Format and expand filename list.""" return [os.path.expanduser(i.format(**kwargs)) for i in paths] diff --git a/serverauditor_sshconfig/handlers.py b/serverauditor_sshconfig/handlers.py index fa62565..e93696e 100644 --- a/serverauditor_sshconfig/handlers.py +++ b/serverauditor_sshconfig/handlers.py @@ -1,4 +1,5 @@ -# coding: utf-8 +# -*- coding: utf-8 -*- +"""Module for connect command.""" from .core.commands import AbstractCommand @@ -6,6 +7,10 @@ class ConnectCommand(AbstractCommand): """Connect to specific host.""" def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(ConnectCommand, self).get_parser(prog_name) parser.add_argument( '-G', '--group', metavar='GROUP_ID or GROUP_NAME', @@ -23,4 +28,5 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): + """Process CLI call.""" self.log.info('Connect to host %s.', parsed_args['host']) diff --git a/serverauditor_sshconfig/main.py b/serverauditor_sshconfig/main.py index 94a6a64..e858ede 100644 --- a/serverauditor_sshconfig/main.py +++ b/serverauditor_sshconfig/main.py @@ -1,11 +1,12 @@ #!/usr/bin/env python -# coding: utf-8 - +# -*- coding: utf-8 -*- +"""Entrypoint for CLI tool.""" import sys from serverauditor_sshconfig.app import ServerauditorApp def main(argv=sys.argv[1:]): + """Process call from terminal.""" app = ServerauditorApp() return app.run(argv) diff --git a/serverauditor_sshconfig/sync/__init__.py b/serverauditor_sshconfig/sync/__init__.py index e69de29..c1e83a0 100644 --- a/serverauditor_sshconfig/sync/__init__.py +++ b/serverauditor_sshconfig/sync/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""Synchronize SaaS and IaaS hosts with application hosts. + +Retrieve service complete host list by service's name and merge the hosts +into application storage. +""" diff --git a/serverauditor_sshconfig/sync/commands.py b/serverauditor_sshconfig/sync/commands.py index 83c8959..1f4b44f 100644 --- a/serverauditor_sshconfig/sync/commands.py +++ b/serverauditor_sshconfig/sync/commands.py @@ -1,10 +1,7 @@ -# coding: utf-8 -from ..core.commands import AbstractCommand +# -*- coding: utf-8 -*- +"""Module with CLI command to retrieve SaaS and IaaS hosts.""" from stevedore.extension import ExtensionManager - - -class NoSuchServiceException(Exception): - pass +from ..core.commands import AbstractCommand class SyncCommand(AbstractCommand): @@ -15,6 +12,10 @@ class SyncCommand(AbstractCommand): ) def get_parser(self, prog_name): + """Create command line argument parser. + + Use it to add extra options to argument parser. + """ parser = super(SyncCommand, self).get_parser(prog_name) parser.add_argument( '-c', '--credentials', @@ -24,6 +25,7 @@ def get_parser(self, prog_name): return parser def get_service(self, service_name): + """Load service instance by name.""" try: extension = self.service_manager[service_name] except KeyError: @@ -33,11 +35,17 @@ def get_service(self, 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(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/serverauditor_sshconfig/sync/services/__init__.py b/serverauditor_sshconfig/sync/services/__init__.py index e69de29..2bff1b9 100644 --- a/serverauditor_sshconfig/sync/services/__init__.py +++ b/serverauditor_sshconfig/sync/services/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Package with logic to retrieve hosts from SaaS and IaaS.""" diff --git a/serverauditor_sshconfig/sync/services/base.py b/serverauditor_sshconfig/sync/services/base.py index ff9ce7c..a9af22a 100644 --- a/serverauditor_sshconfig/sync/services/base.py +++ b/serverauditor_sshconfig/sync/services/base.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Acquire SaaS and IaaS hosts.""" import abc import six from ...core.storage import ApplicationStorage @@ -5,8 +7,10 @@ @six.add_metaclass(abc.ABCMeta) class BaseSyncService(object): + """Base class for acquiring SaaS and IaaS hosts into storage.""" def __init__(self, application_name, crendetial): + """Construct new instance for providing hosts from SaaS and IaaS.""" self.crendetial = crendetial self.storage = ApplicationStorage(application_name) @@ -15,6 +19,7 @@ def hosts(self): """Override to return host instances.""" def sync(self): + """Sync storage content and the Service hosts.""" service_hosts = self.hosts() with self.storage: for i in service_hosts: diff --git a/tox.ini b/tox.ini index 1f6debc..168a4b2 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = mock nose coverage - prospector + prospector[with_pyroma] commands = pip install -U . paver nosetests From 886bf502fb0e8f4e3babf437ebcb81ac31ebf92f Mon Sep 17 00:00:00 2001 From: EvgeneOskin Date: Wed, 27 Jan 2016 22:15:38 +0600 Subject: [PATCH 10/10] Add pre-commit config. --- .pre-commit-config.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7767e9c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +- repo: https://github.com/pre-commit/pre-commit-hooks + sha: 64943e86417774b9d6ba63c74e00f8cc3e2119e0 + hooks: + - id: check-ast + - id: check-added-large-files + - id: check-case-conflict + - id: check-docstring-first + - id: check-merge-conflict + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: double-quote-string-fixer + - id: fix-encoding-pragma + - id: trailing-whitespace + - id: name-tests-test + +- repo: https://github.com/guykisel/prospector-mirror + sha: 00fbd80101566b1b9c873c71f2ab7b95b8bd0a7d + hooks: + - id: prospector