Example #1
0
    def run(self):
        if not type(self)._command_runners:
            # avoid circular dependency between this module and command_runners
            type(self)._command_runners = utils.import_module('devassistant.command_runners')
        for cr in type(self)._command_runners.command_runners:
            if cr.matches(self):
                return cr.run(self) 

        raise exceptions.CommandException('No runner for command "{ct}: {c}".'.\
                format(ct=self.comm_type,
                       c=self.comm))
Example #2
0
    def format_deep(self, eval_expressions=True):
        """Formats command input of this command as a Python structure (list/dict/str).

        Args:
            eval_expressions: if False, format_deep will only substitute variables, but won't
                              evaluate expressions
        """
        if not self._lang:
            # avoid circular dependency between this module and lang
            type(self)._lang = utils.import_module('devassistant.lang')

        return self._format_deep_recursive(struct=self.comm, eval_expressions=eval_expressions)
Example #3
0
class DockerHelper(object):

    try:
        _docker_module = utils.import_module('docker')
    except:
        _docker_module = None

    @classmethod
    def is_available(cls):
        return cls._docker_module is not None

    @property
    def errors(cls):
        try:
            return cls._docker_module.errors
        except AttributeError:
            return None

    @classmethod
    def get_client(cls):
        try:
            return cls._docker_module.Client()
        except AttributeError:
            return None

    @classmethod
    def docker_service_running(cls):
        try:
            ClHelper.run_command('systemctl status docker')
            return True
        except exceptions.ClException:
            return False

    @classmethod
    def docker_service_enable_and_run(cls):
        # TODO: add some conditionals for various platforms
        logger.info('Enabling and running docker service ...')
        try:
            cmd_str = 'bash -c "systemctl enable docker && systemctl start docker"'
            ClHelper.run_command(cmd_str, as_user='******')
        except exceptions.ClException:
            raise exceptions.CommandException(
                'Failed to enable and run docker service.')

        # we need to wait until /var/run/docker.sock is created
        # let's wait for 30 seconds
        logger.info(
            'Waiting for /var/run/docker.sock to be created (max 15 seconds) ...'
        )
        success = False
        for i in range(0, 30):
            time.sleep(i * 0.5)
            try:
                ClHelper.run_command('ls /var/run/docker.sock')
                success = True
                break
            except exceptions.ClException:
                pass

        if not success:
            logger.warning(
                '/var/run/docker.sock doesn\'t exist, docker will likely not work!'
            )

    @classmethod
    def docker_group_active(cls):
        logger.debug(
            'Determining if current user has active "docker" group ...')
        # we have to run cl command, too see if the user has already re-logged
        # after being added to docker group, so that he can effectively use it
        if 'docker' in ClHelper.run_command('groups').split():
            logger.debug('Current user is in "docker" group.')
            return True
        else:
            logger.debug('Current user is not in "docker" group.')
            return False

    @classmethod
    def user_in_docker_group(cls, username):
        if grp:
            return username in grp.getgrnam('docker').gr_mem
        else:  # NotUNIX
            return False

    @classmethod
    def add_user_to_docker_group(cls, username):
        try:
            logger.info('Adding {0} to group docker ...'.format(username))
            ClHelper.run_command(
                'bash -c "usermod -a -G docker {0}"'.format(username),
                as_user='******')
        except exceptions.ClException as e:
            msg = 'Failed to add user to "docker" group: {0}'.format(e.output)
            raise exceptions.CommandException(msg)
Example #4
0
class GitHubAuth(object):
    """Only use the github_authenticated decorator from the class.
    The other methods should be consider private; they expect certain order of calling,
    so calling them ad-hoc may break something.
    """
    _user = None
    _token = None
    try:
        _gh_module = utils.import_module('github')
    except:
        _gh_module = None

    @classmethod
    def _github_token(cls, login):
        if not cls._token:
            try:
                cls._token = ClHelper.run_command("git config github.token.{login}".format(
                    login=login))
            except exceptions.ClException:
                pass  # token is not available yet

        return cls._token

    @classmethod
    def _get_github_user(cls, login):
        if not cls._user:
            try:
                # try logging with token
                token = cls._github_token(login)
                gh = cls._gh_module.Github(login_or_token=token)
                cls._user = gh.get_user()
                # try if the authentication was successful
                cls._user.login
            except cls._gh_module.GithubException:
                # if the token was set, it was wrong, so make sure it's reset
                cls._token = None
                # try login with username/password 3 times
                cls._user = cls._try_login_with_password_ntimes(login, 3)
                if cls._user is not None:
                    cls._github_create_auth()  # create auth for future use
        return cls._user

    @classmethod
    def _try_login_with_password_ntimes(cls, login, ntimes):
        user = None

        for i in range(0, ntimes):
            password = DialogHelper.ask_for_password(
                prompt='Github Password for {username}:'.format(username=login))

            # user pressed Ctrl + D
            if password is None:
                break

            gh = cls._gh_module.Github(login_or_token=login, password=password)
            user = gh.get_user()
            try:
                user.login
                break  # if user.login doesn't raise, authentication has been successful
            except cls._gh_module.GithubException as e:
                user = None
                msg = 'Wrong Github username or password; message from Github: {0}\n'.\
                    format(e.data.get('message', 'Unknown authentication error'))
                msg += 'Try again or press {0} to abort.'
                if current_run.UI == 'cli':
                    msg = msg.format('Ctrl + D')
                else:
                    msg = msg.format('"Cancel"')
                logger.warning(msg)

        return user

    @classmethod
    def _github_create_auth(cls):
        """ Store token into ~/.gitconfig.

        Note: this uses cls._user.get_authorizations(), which only works if cls._user
        was authorized by login/password, doesn't work for token auth (TODO: why?).
        If token is not defined then store it into ~/.gitconfig file
        """
        if not cls._token:
            try:
                auth = None
                for a in cls._user.get_authorizations():
                    if a.note == 'DevAssistant':
                        auth = a
                if not auth:
                    auth = cls._user.create_authorization(
                        scopes=['repo', 'user', 'admin:public_key'],
                        note="DevAssistant")
                ClHelper.run_command("git config --global github.token.{login} {token}".format(
                    login=cls._user.login,
                    token=auth.token))
                ClHelper.run_command("git config --global github.user.{login} {login}".format(
                    login=cls._user.login))
            except cls._gh_module.GithubException as e:
                logger.warning('Creating authorization failed: {0}'.format(e))

    @classmethod
    def _github_create_ssh_key(cls):
        """Creates a local ssh key, if it doesn't exist already, and uploads it to Github."""
        try:
            login = cls._user.login
            pkey_path = '{home}/.ssh/{keyname}'.format(home=os.path.expanduser('~'),
                        keyname=settings.GITHUB_SSH_KEYNAME.format(login=login))
            # generate ssh key only if it doesn't exist
            if not os.path.exists(pkey_path):
                ClHelper.run_command('ssh-keygen -t rsa -f {pkey_path}\
                                     -N \"\" -C \"DevAssistant\"'.\
                                     format(pkey_path=pkey_path))
            ClHelper.run_command('ssh-add {pkey_path}'.format(pkey_path=pkey_path))
            public_key = ClHelper.run_command('cat {pkey_path}.pub'.format(pkey_path=pkey_path))
            cls._user.create_key("DevAssistant", public_key)
        except exceptions.ClException as e:
            msg = 'Couldn\'t create a new ssh key: {e}'.format(e)
            raise exceptions.CommandException(msg)

    @classmethod
    def _create_ssh_config_entry(cls):
        # TODO: some duplication with _ssh_key_needs_config_entry, maybe refactor a bit
        ssh_config = os.path.expanduser('~/.ssh/config')
        fh = os.fdopen(os.open(ssh_config, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600), 'a')
        fh.write(settings.GITHUB_SSH_CONFIG.format(
                 login=cls._user.login,
                 keyname=settings.GITHUB_SSH_KEYNAME.format(login=cls._user.login)))
        fh.close()

    @classmethod
    def _github_ssh_key_exists(cls):
        """Returns True if any key on Github matches a local key, else False."""
        remote_keys = map(lambda k: k._key, cls._user.get_keys())
        found = False
        pubkey_files = glob.glob(os.path.expanduser('~/.ssh/*.pub'))
        for rk in remote_keys:
            for pkf in pubkey_files:
                local_key = open(pkf).read()
                # in PyGithub 1.23.0, remote key is an object, not string
                rkval = rk if isinstance(rk, six.string_types) else rk.value
                # don't use "==" because we have comments etc added in public_key
                if rkval in local_key:
                    found = True
                    break
        return found

    @classmethod
    def _ssh_key_needs_config_entry(cls):
        if getpass.getuser() != cls._user.login:
            ssh_config = os.path.expanduser('~/.ssh/config')
            user_github_string = 'github.com-{0}'.format(cls._user.login)
            needs_to_add_config_entry = True

            if os.path.isfile(ssh_config):
                fh = open(ssh_config)
                config_content = fh.read()
                if user_github_string in config_content:
                    needs_to_add_config_entry = False
                fh.close()
            return needs_to_add_config_entry
        return False

    @classmethod
    def github_authenticated(cls, func):
        """Does user authentication, creates SSH keys if needed and injects "_user" attribute
        into class/object bound to the decorated function.
        Don't call any other methods of this class manually, this should be everything you need.
        """
        def inner(func_cls, *args, **kwargs):
            if not cls._gh_module:
                logger.warning('PyGithub not installed, skipping Github auth procedures.')
            elif not func_cls._user:
                # authenticate user, possibly also creating authentication for future use
                func_cls._user = cls._get_github_user(kwargs['login'])
                if func_cls._user is None:
                    msg = 'Github authentication failed, skipping Github command.'
                    logger.warning(msg)
                    return (False, msg)
                # create an ssh key for pushing if we don't have one
                if not cls._github_ssh_key_exists():
                    cls._github_create_ssh_key()
                # next, create ~/.ssh/config entry for the key, if system username != GH login
                if cls._ssh_key_needs_config_entry():
                    cls._create_ssh_config_entry()
            return func(func_cls, *args, **kwargs)

        return inner
Example #5
0
class GitHubAuth(object):
    """Only use the github_authenticated decorator from the class.
    The other methods should be consider private; they expect certain order of calling,
    so calling them ad-hoc may break something.
    """
    _user = None
    _token = None
    try:
        _gh_module = utils.import_module('github')
        _gh_exceptions = utils.import_module('github.GithubException')

    except:
        _gh_module = None
        _gh_exceptions = None

    @classmethod
    def _github_token(cls, login):
        if not cls._token:
            try:
                cls._token = ClHelper.run_command(
                    "git config github.token.{login}".format(login=login),
                    log_secret=True)
            except exceptions.ClException:
                pass  # token is not available yet

        return cls._token

    @classmethod
    def _get_github_user(cls, login, ui):
        if not cls._github_token(login):
            cls._user = cls._try_login_with_password_ntimes(login, 3, ui)
        else:
            try:  # try logging with token
                token = cls._github_token(login)
                gh = cls._gh_module.Github(login_or_token=token)
                user = gh.get_user()
                user.login  # throws unless the authentication was successful
                cls._user = user
            except cls._gh_module.GithubException:
                # if the token was set, it was wrong, so make sure it's reset
                cls._token = None
                cls._user = cls._try_login_with_password_ntimes(login, 3, ui)

        if cls._token is None:
            cls._github_create_authorization(ui)

        return cls._user

    @classmethod
    def _github_create_authorization(cls, ui):
        try:
            cls._user.login
            cls._github_create_simple_authorization()
        except cls._gh_exceptions.TwoFactorException:
            cls._github_create_twofactor_authorization(ui)
        except cls._gh_exceptions.GithubException:
            raise

    @classmethod
    def _try_login_with_password_ntimes(cls, login, ntimes, ui):
        user = None

        for i in range(0, ntimes):
            password = DialogHelper.ask_for_password(
                ui,
                prompt='Github Password for {username}:'.format(
                    username=login))

            # user pressed Ctrl + D
            if password is None:
                break

            gh = cls._gh_module.Github(login_or_token=login, password=password)
            user = gh.get_user()
            try:
                user.login
                break  # if user.login doesn't raise, authentication has been successful
            except cls._gh_exceptions.TwoFactorException:
                break  # two-factor auth is used
            except cls._gh_module.GithubException as e:
                user = None
                msg = 'Wrong Github username or password; message from Github: {0}\n'.\
                    format(e.data.get('message', 'Unknown authentication error'))
                msg += 'Try again or press {0} to abort.'
                if ui == 'cli':
                    msg = msg.format('Ctrl + D')
                else:
                    msg = msg.format('"Cancel"')
                logger.warning(msg)

        return user

    @classmethod
    def _github_create_twofactor_authorization(cls, ui):
        """Create an authorization for a GitHub user using two-factor
           authentication. Unlike its non-two-factor counterpart, this method
           does not traverse the available authentications as they are not
           visible until the user logs in.

           Please note: cls._user's attributes are not accessible until the
           authorization is created due to the way (py)github works.
        """
        try:
            try:  # This is necessary to trigger sending a 2FA key to the user
                auth = cls._user.create_authorization()
            except cls._gh_exceptions.GithubException:
                onetime_pw = DialogHelper.ask_for_password(
                    ui, prompt='Your one time password:'******'repo', 'user', 'admin:public_key'],
                    note="DevAssistant",
                    onetime_password=onetime_pw)
                cls._user = cls._gh_module.Github(
                    login_or_token=auth.token).get_user()
                logger.debug(
                    'Two-factor authorization for user "{0}" created'.format(
                        cls._user.login))
                cls._github_store_authorization(cls._user, auth)
                logger.debug('Two-factor authorization token stored')
        except cls._gh_exceptions.GithubException as e:
            logger.warning(
                'Creating two-factor authorization failed: {0}'.format(e))

    @classmethod
    def _github_create_simple_authorization(cls):
        """Create a GitHub authorization for the given user in case they don't
           already have one.
        """
        try:
            auth = None
            for a in cls._user.get_authorizations():
                if a.note == 'DevAssistant':
                    auth = a
            if not auth:
                auth = cls._user.create_authorization(
                    scopes=['repo', 'user', 'admin:public_key'],
                    note="DevAssistant")
                cls._github_store_authorization(cls._user, auth)
        except cls._gh_exceptions.GithubException as e:
            logger.warning('Creating authorization failed: {0}'.format(e))

    @classmethod
    def _github_store_authorization(cls, user, auth):
        """Store an authorization token for the given GitHub user in the git
           global config file.
        """
        ClHelper.run_command(
            "git config --global github.token.{login} {token}".format(
                login=user.login, token=auth.token),
            log_secret=True)
        ClHelper.run_command(
            "git config --global github.user.{login} {login}".format(
                login=user.login))

    @classmethod
    def _start_ssh_agent(cls):
        """Starts ssh-agent and returns the environment variables related to it"""
        env = dict()
        stdout = ClHelper.run_command('ssh-agent -s')
        lines = stdout.split('\n')
        for line in lines:
            if not line or line.startswith('echo '):
                continue
            line = line.split(';')[0]
            parts = line.split('=')
            if len(parts) == 2:
                env[parts[0]] = parts[1]
        return env

    @classmethod
    def _github_create_ssh_key(cls):
        """Creates a local ssh key, if it doesn't exist already, and uploads it to Github."""
        try:
            login = cls._user.login
            pkey_path = '{home}/.ssh/{keyname}'.format(
                home=os.path.expanduser('~'),
                keyname=settings.GITHUB_SSH_KEYNAME.format(login=login))
            # generate ssh key only if it doesn't exist
            if not os.path.exists(pkey_path):
                ClHelper.run_command('ssh-keygen -t rsa -f {pkey_path}\
                                     -N \"\" -C \"DevAssistant\"'.format(
                    pkey_path=pkey_path))
            try:
                ClHelper.run_command(
                    'ssh-add {pkey_path}'.format(pkey_path=pkey_path))
            except exceptions.ClException:
                # ssh agent might not be running
                env = cls._start_ssh_agent()
                ClHelper.run_command(
                    'ssh-add {pkey_path}'.format(pkey_path=pkey_path), env=env)
            public_key = ClHelper.run_command(
                'cat {pkey_path}.pub'.format(pkey_path=pkey_path))
            cls._user.create_key("DevAssistant", public_key)
        except exceptions.ClException as e:
            msg = 'Couldn\'t create a new ssh key: {0}'.format(e)
            raise exceptions.CommandException(msg)

    @classmethod
    def _create_ssh_config_entry(cls):
        ssh_config = os.path.expanduser('~/.ssh/config')
        fh = os.fdopen(
            os.open(ssh_config, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600),
            'a')
        fh.write(
            settings.GITHUB_SSH_CONFIG.format(
                login=cls._user.login,
                keyname=settings.GITHUB_SSH_KEYNAME.format(
                    login=cls._user.login)))
        fh.close()

    @classmethod
    def _github_ssh_key_exists(cls):
        """Returns True if any key on Github matches a local key, else False."""
        remote_keys = map(lambda k: k._key, cls._user.get_keys())
        found = False
        pubkey_files = glob.glob(os.path.expanduser('~/.ssh/*.pub'))
        for rk in remote_keys:
            for pkf in pubkey_files:
                local_key = io.open(pkf, encoding='utf-8').read()
                # in PyGithub 1.23.0, remote key is an object, not string
                rkval = rk if isinstance(rk, six.string_types) else rk.value
                # don't use "==" because we have comments etc added in public_key
                if rkval in local_key:
                    found = True
                    break
        return found

    @classmethod
    def _ssh_key_needs_config_entry(cls):
        if getpass.getuser() != cls._user.login:
            ssh_config = os.path.expanduser('~/.ssh/config')
            user_github_string = 'github.com-{0}'.format(cls._user.login)
            needs_to_add_config_entry = True

            if os.path.isfile(ssh_config):
                fh = open(ssh_config)
                config_content = fh.read()
                if user_github_string in config_content:
                    needs_to_add_config_entry = False
                fh.close()
            return needs_to_add_config_entry
        return False

    @classmethod
    def github_authenticated(cls, func):
        """Does user authentication, creates SSH keys if needed and injects "_user" attribute
        into class/object bound to the decorated function.
        Don't call any other methods of this class manually, this should be everything you need.
        """
        def inner(func_cls, *args, **kwargs):
            if not cls._gh_module:
                logger.warning(
                    'PyGithub not installed, skipping Github auth procedures.')
            elif not func_cls._user:
                # authenticate user, possibly also creating authentication for future use
                login = kwargs['login'].encode(
                    utils.defenc) if not six.PY3 else kwargs['login']
                func_cls._user = cls._get_github_user(login, kwargs['ui'])
                if func_cls._user is None:
                    msg = 'Github authentication failed, skipping Github command.'
                    logger.warning(msg)
                    return (False, msg)
                # create an ssh key for pushing if we don't have one
                if not cls._github_ssh_key_exists():
                    cls._github_create_ssh_key()
                # next, create ~/.ssh/config entry for the key, if system username != GH login
                if cls._ssh_key_needs_config_entry():
                    cls._create_ssh_config_entry()
            return func(func_cls, *args, **kwargs)

        return inner
Example #6
0
 def load_command_runners(cls):
     if not cls.command_runners:
         cls.command_runners = utils.import_module('devassistant.command_runners')
     return cls.command_runners
Example #7
0
class GitHubAuth(object):
    """Only use the github_authenticated decorator from the class.
    The other methods should be consider private; they expect certain order of calling,
    so calling them ad-hoc may break something.
    """
    _user = None
    _token = None
    try:
        _gh_module = utils.import_module('github')
    except:
        _gh_module = None

    @classmethod
    def _github_token(cls, login):
        if not cls._token:
            try:
                cls._token = ClHelper.run_command("git config github.token.{login}".format(
                    login=login))
            except exceptions.ClException:
                pass # token is not available yet

        return cls._token

    @classmethod
    def _get_github_user(cls, login):
        if not cls._user:
            try:
                # try logging with token
                token = cls._github_token(login)
                gh = cls._gh_module.Github(login_or_token=token)
                cls._user = gh.get_user()
                # try if the authentication was successful
                cls._user.login
            except cls._gh_module.GithubException:
                # if the token was set, it was wrong, so make sure it's reset
                cls._token = None
                # login with username/password
                password = DialogHelper.ask_for_password(
                        prompt='Github Password for {username}:'.format(username=login))
                gh = cls._gh_module.Github(login_or_token=login, password=password)
                cls._user = gh.get_user()
                try:
                    cls._user.login
                    cls._github_create_auth() # create auth for future use
                except cls._gh_module.GithubException as e:
                    msg = 'Wrong username or password\nGitHub exception: {0}'.format(e)
                    # reset cls._user to None, so that we don't use it if calling this multiple times
                    cls._user = None
                    raise exceptions.CommandException(msg)
        return cls._user

    @classmethod
    def _github_create_auth(cls):
        """ Store token into ~/.gitconfig.

        If token is not defined then store it into ~/.gitconfig file
        """
        if not cls._token:
            try:
                auth = cls._user.create_authorization(scopes=['repo', 'user'], note="DeveloperAssistant")
                ClHelper.run_command("git config --global github.token.{login} {token}".format(
                    login=cls._user.login,
                    token=auth.token))
                ClHelper.run_command("git config --global github.user.{login} {login}".format(
                    login=cls._user.login))
            except cls._gh_module.GithubException as e:
                logger.warning('Creating authorization failed: {0}'.format(e))

    @classmethod
    def _github_create_ssh_key(cls):
        try:
            login = cls._user.login
            pkey_path = '{home}/.ssh/{keyname}'.format(home=os.path.expanduser('~'),
                                                       keyname=settings.GITHUB_SSH_KEYNAME.format(login=login))
            # TODO: handle situation where {pkey_path} exists, but it's not registered on GH
            # generate ssh key
            ClHelper.run_command('ssh-keygen -t rsa -f {pkey_path}\
                                 -N \"\" -C \"DevAssistant\"'.\
                                 format(pkey_path=pkey_path))
            ClHelper.run_command('ssh-add {pkey_path}'.format(pkey_path=pkey_path))
            public_key = ClHelper.run_command('cat {pkey_path}.pub'.format(pkey_path=pkey_path))
            cls._user.create_key("devassistant", public_key)
        except exceptions.ClException as e:
            msg = 'Couldn\'t create a new ssh key: {e}'.format(e)
            raise exceptions.CommandException(msg)

    @classmethod
    def _create_ssh_config_entry(cls):
        # TODO: some duplication with _ssh_key_needs_config_entry, maybe refactor a bit
        ssh_config = os.path.expanduser('~/.ssh/config')
        fh = os.fdopen(os.open(ssh_config, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0o600), 'a')
        fh.write(settings.GITHUB_SSH_CONFIG.format(
                 login=cls._user.login,
                 keyname=settings.GITHUB_SSH_KEYNAME.format(login=cls._user.login)))
        fh.close()

    @classmethod
    def _github_ssh_key_exists(cls):
        remote_keys = map(lambda k: k._key, cls._user.get_keys())
        found = False
        pubkey_files = glob.glob(os.path.expanduser('~/.ssh/*.pub'))
        for rk in remote_keys:
            for pkf in pubkey_files:
                local_key = open(pkf).read()
                # don't use "==" because we have comments etc added in public_key
                # in PyGithub 1.23.0, remote key is an object, not string
                rkval = rk if isinstance(rk, six.string_types) else rk.value
                if rkval in local_key:
                    found = True
                    break
        return found

    @classmethod
    def _ssh_key_needs_config_entry(cls):
        if getpass.getuser() != cls._user.login:
            ssh_config = os.path.expanduser('~/.ssh/config')
            user_github_string = 'github.com-{0}'.format(cls._user.login)
            needs_to_add_config_entry = True

            if os.path.isfile(ssh_config):
                fh = open(ssh_config)
                config_content = fh.read()
                if user_github_string in config_content:
                    needs_to_add_config_entry = False
                fh.close()
            return needs_to_add_config_entry
        return False


    @classmethod
    def github_authenticated(cls, func):
        """Does user authentication, creates SSH keys if needed and injects "_user" attribute
        into class/object bound to the decorated function.
        Don't call any other methods of this class manually, this should be everything you need.
        """
        def inner(func_cls, *args, **kwargs):
            if not cls._gh_module:
                logger.warning('PyGithub not installed, skipping github authentication procedures.')
            elif not func_cls._user:
                # authenticate user, possibly also creating authentication for future use
                func_cls._user = cls._get_github_user(kwargs['login'])
                # create an ssh key for pushing if we don't have one
                if not cls._github_ssh_key_exists():
                    cls._github_create_ssh_key()
                # next, create ~/.ssh/config entry for the key, if system username != GH login
                if cls._ssh_key_needs_config_entry():
                    cls._create_ssh_config_entry()
            return func(func_cls, *args, **kwargs)

        return inner
Example #8
0
 def load_command_runners(cls):
     if not cls.command_runners:
         cls.command_runners = utils.import_module(
             'devassistant.command_runners')
     return cls.command_runners
Example #9
0
class GitHubCommandRunner(CommandRunner):
    _user = None
    try:
        _gh_module = utils.import_module('github')
    except:
        _gh_module = None
    _required_yaml_args = {'default': ['login', 'reponame'],
                           'create_repo': ['login', 'reponame', 'private'],
                           'create_and_push': ['login', 'reponame', 'private'],
                           'create_fork': ['login', 'repo_url'],
                           'push': []}

    @classmethod
    def matches(cls, c):
        return c.comm_type == 'github'

    @classmethod
    def run(cls, c):
        """Arguments given to 'github' command may be:
        - Just a string (action), which implies that all the other arguments are deducted from
          global context and local system.
        - List containing a string (action) as a first item and rest of the args in a dict.
          (args not specified in the dict are taken from global context.

        Possible arguments:
        - login - taken from 'github' or system username - represents Github login
        - reponame - taken from 'name' (first applies os.path.basename) - repo to operate on
        """
        comm, kwargs = cls.format_args(c)
        if not cls._gh_module:
            logger.warning('PyGithub not installed, cannot execute github command.')
            return [False, '']

        # we pass arguments as kwargs, so that the auth decorator can easily query them
        # NOTE: these are not the variables from global context, but rather what
        # cls.format_args returned
        if comm == 'create_repo':
            ret = cls._github_create_repo(**kwargs)
        elif comm == 'push':
            ret = cls._github_push()
        elif comm == 'create_and_push':
            ret = cls._github_create_and_push(**kwargs)
        elif comm == 'add_remote_origin':
            ret = cls._github_add_remote_origin(**kwargs)
        elif comm == 'create_fork':
            ret = cls._github_fork(**kwargs)
        else:
            raise exceptions.CommandException('Unknown command type {ct}.'.format(ct=c.comm_type))

        return ret

    @classmethod
    def format_args(cls, c):
        args = c.input_res
        if isinstance(args, list):
            comm = args[0]
            args_rest = args[1]
        else:
            comm = args
            args_rest = {}
        # find out what arguments we will need
        kwargs = {}
        req_kwargs = cls._required_yaml_args.get(comm, cls._required_yaml_args['default'])
        for k in req_kwargs:
            kwargs[k] = getattr(cls, '_guess_' + k)(args_rest.get(k), c.kwargs)

        return comm, kwargs

    @classmethod
    def _guess_login(cls, explicit, ctxt):
        """Get github login, either from explicitly given string or 'github' global variable
        or from local username.

        Args:
            ctxt: global context

        Returns:
            guessed github login
        """
        return explicit or ctxt.get('github', None) or getpass.getuser()

    @classmethod
    def _guess_reponame(cls, explicit, ctxt):
        """Extract reponame, either from explicitly given string or from 'name' global variable,
        which is possibly a path.

        Args:
            ctxt: global context

        Returns:
            guessed reponame
        """
        name = explicit
        if not name:
            name = os.path.basename(ctxt.get('name', ''))
        if not name:
            raise exceptions.CommandException('Cannot guess Github reponame - no argument given'
                                              'and there is no "name" variable.')
        return name

    @classmethod
    def _guess_repo_url(cls, explicit, ctxt):
        """Get repo to fork in form of '<login>/<reponame>' from explicitly given string or
        global variable 'url'.

        Args:
            ctxt: global context

        Returns:
            guessed fork reponame
        """
        url = explicit or ctxt.get('url')
        if not url:
            raise exceptions.CommandException('Cannot guess name of Github repo to fork - no'
                                              'argument given and there is no "url" variable.')

        url = url[:-4] if url.endswith('.git') else url
        # if using git@github:username/reponame.git, strip the stuff before ":"
        url = url.split(':')[-1]
        return '/'.join(url.split('/')[-2:])

    @classmethod
    def _guess_private(cls, explicit, ctxt):
        return bool(explicit or ctxt.get('github_private') or False)

    @classmethod
    def _github_push(cls):
        try:
            ret = ClHelper.run_command("git push -u origin master")
            logger.info('Source code was successfully pushed.')
            return (True, ret)
        except exceptions.ClException as e:
            logger.warning('Problem pushing source code: {0}'.format(e.output))
            return (False, e.output)

    @classmethod
    @GitHubAuth.github_authenticated
    def _github_add_remote_origin(cls, **kwargs):
        """Note: the kwargs are not the global context here, but what cls.format_args returns."""
        reponame = kwargs['reponame']
        login = kwargs['login']
        # if system username != GH login, we need to use [email protected]{login}:...
        # else just [email protected]:...
        dash_login = ''
        if getpass.getuser() != login:
            dash_login = '******' + login
        try:
            logger.info('Adding Github repo as git remote ...')
            ret = ClHelper.run_command("git remote add origin [email protected]{dl}:{l}/{r}.git".\
                format(dl=dash_login, l=login, r=reponame))
            logger.info('Successfully added Github repo as git remote.')
            return (True, ret)
        except exceptions.ClException as e:
            logger.warning('Problem adding Github repo as git remote: {0}.'.format(e.output))
            return (False, e.output)

    @classmethod
    @GitHubAuth.github_authenticated
    def _github_create_repo(cls, **kwargs):
        """Create repo on GitHub.
        Note: the kwargs are not the global context here, but what cls.format_args returns.

        If repository already exists then CommandException will be raised.

        Raises:
            devassistant.exceptions.CommandException on error
        """
        reponame = kwargs['reponame']

        if reponame in map(lambda x: x.name, cls._user.get_repos()):
            msg = 'Failed to create Github repo: {0}/{1} alread exists.'.\
                format(cls._user.login, reponame)
            logger.warning(msg)
            return (False, msg)
        else:
            msg = ''
            success = False
            try:
                new_repo = cls._user.create_repo(reponame, private=kwargs['private'])
                msg = new_repo.clone_url
                success = True
            except cls._gh_module.GithubException as e:
                gh_errs = e.data.get('errors', [])
                gh_errs = '; '.join(map(lambda err: err.get('message', ''), gh_errs))
                msg = 'Failed to create GitHub repo. This sometime happens when you delete '
                msg += 'a repo and then you want to create the same one immediately. If that\'s '
                msg += 'the case, wait for few minutes and then try again.\n'
                msg += 'Github errors: ' + gh_errs
            except BaseException as e:
                msg = 'Failed to create Github repo: {0}'.\
                    format(getattr(e, 'message', 'Unknown error'))

            if success:
                logger.info('Your new repository: {0}'.format(new_repo.html_url))
            else:
                logger.warning(msg)

        return (success, msg)

    @classmethod
    @GitHubAuth.github_authenticated
    def _github_add_remote_and_push(cls, **kwargs):
        """Add a remote and push to GitHub. As this is not a callable subcommand of this
        command runner, it doesn't emit any informative logging messages on its own, only messages
        emitted by called methods.
        Note: the kwargs are not the global context here, but what cls.format_args returns.
        """
        ret = cls._github_add_remote_origin(**kwargs)
        if ret[0]:
            ret = cls._github_push()
        return ret

    @classmethod
    @GitHubAuth.github_authenticated
    def _github_create_and_push(cls, **kwargs):
        """Note: the kwargs are not the global context here, but what cls.format_args returns."""
        # we assume we're in the project directory
        logger.info('Registering your {priv}project on GitHub as {login}/{repo}...'.\
                format(priv='private ' if kwargs['private'] else '',
                       login=kwargs['login'],
                       repo=kwargs['reponame']))
        ret = cls._github_create_repo(**kwargs)
        if ret[0]:  # on success push the sources
            ret = cls._github_add_remote_and_push(**kwargs)
        return ret

    @classmethod
    @GitHubAuth.github_authenticated
    def _github_fork(cls, **kwargs):
        """Create a fork of repo from kwargs['fork_repo'].
        Note: the kwargs are not the global context here, but what cls.format_args returns.

        Raises:
            devassistant.exceptions.CommandException on error
        """
        timeout = 300 # 5 minutes
        fork_login, fork_reponame = kwargs['repo_url'].split('/')
        logger.info('Forking {repo} for user {login} on Github ...'.\
            format(login=kwargs['login'], repo=kwargs['repo_url']))
        success = False
        msg = ''
        try:
            repo = cls._gh_module.Github().get_user(fork_login).get_repo(fork_reponame)
            fork = cls._user.create_fork(repo)
            while timeout > 0:
                time.sleep(5)
                timeout -= 5
                try:
                    fork.get_contents('/') # This function doesn't throw an exception when clonable
                    success = True
                    break
                except cls._gh_module.GithubException as e:
                    if 'is empty' not in str(e):
                        raise e
            msg = fork.ssh_url
        except cls._gh_module.GithubException as e:
            msg = 'Failed to create Github fork with error: {err}'.format(err=e)
        except BaseException as e:
            msg = 'Exception while forking GH repo: {0}'.\
                format(getattr(e, 'message', 'Unknown error'))

        if success:
            logger.info('Fork is ready at {url}.'.format(url=fork.html_url))
        else:
            logger.warning(msg)

        return (success, msg)