Ejemplo n.º 1
0
    def test_run_shell_command_masked(self, fake_ssh_client, fake_click):
        fake_ssh = MagicMock()
        fake_stdout = MagicMock()
        fake_ssh.exec_command.return_value = MagicMock(
        ), fake_stdout, MagicMock()

        fake_ssh_client.return_value = fake_ssh

        inventory = make_inventory(hosts=("somehost", ))
        State(inventory, Config())
        host = inventory.get_host("somehost")
        host.connect()

        command = StringCommand("echo", MaskString("top-secret-stuff"))
        fake_stdout.channel.recv_exit_status.return_value = 0

        out = host.run_shell_command(command,
                                     print_output=True,
                                     print_input=True)
        assert len(out) == 3

        status, stdout, stderr = out
        assert status is True

        fake_ssh.exec_command.assert_called_with(
            "sh -c 'echo top-secret-stuff'",
            get_pty=False,
        )

        fake_click.echo.assert_called_with(
            "{0}>>> sh -c 'echo ***'".format(host.print_prefix),
            err=True,
        )
Ejemplo n.º 2
0
def make_mysql_command(
    database=None,
    user=None,
    password=None,
    host=None,
    port=None,
    executable="mysql",
):
    target_bits = [executable]

    if database:
        target_bits.append(database)

    if user:
        # Quote the username as in may contain special characters
        target_bits.append('-u"{0}"'.format(user))

    if password:
        # Quote the password as it may contain special characters
        target_bits.append(MaskString('-p"{0}"'.format(password)))

    if host:
        target_bits.append("-h{0}".format(host))

    if port:
        target_bits.append("-P{0}".format(port))

    return StringCommand(*target_bits)
Ejemplo n.º 3
0
    def test_run_shell_command_masked(self, fake_click):
        inventory = make_inventory(hosts=('@local', ))
        state = State(inventory, Config())
        host = inventory.get_host('@local')

        command = StringCommand('echo', MaskString('top-secret-stuff'))
        self.fake_popen_mock().returncode = 0

        out = host.run_shell_command(state,
                                     command,
                                     print_output=True,
                                     print_input=True)
        assert len(out) == 3

        status, stdout, stderr = out
        assert status is True

        self.fake_popen_mock.assert_called_with(
            "sh -c 'echo top-secret-stuff'",
            shell=True,
            stdout=PIPE,
            stderr=PIPE,
            stdin=PIPE,
        )

        fake_click.echo.assert_called_with(
            "{0}>>> sh -c 'echo ***'".format(host.print_prefix), )
Ejemplo n.º 4
0
def make_psql_command(
    database=None,
    user=None,
    password=None,
    host=None,
    port=None,
    executable='psql',
):
    target_bits = []

    if password:
        target_bits.append(MaskString('PGPASSWORD="******"'.format(password)))

    target_bits.append(executable)

    if database:
        target_bits.append('-d {0}'.format(database))

    if user:
        target_bits.append('-U {0}'.format(user))

    if host:
        target_bits.append('-h {0}'.format(host))

    if port:
        target_bits.append('-p {0}'.format(port))

    return StringCommand(*target_bits)
Ejemplo n.º 5
0
def make_unix_command(
    command,
    env=None,
    chdir=None,
    shell_executable=Config.SHELL,
    # Su config
    su_user=Config.SU_USER,
    use_su_login=Config.USE_SU_LOGIN,
    su_shell=Config.SU_SHELL,
    preserve_su_env=Config.PRESERVE_SU_ENV,
    # Sudo config
    sudo=Config.SUDO,
    sudo_user=Config.SUDO_USER,
    use_sudo_login=Config.USE_SUDO_LOGIN,
    use_sudo_password=Config.USE_SUDO_PASSWORD,
    preserve_sudo_env=Config.PRESERVE_SUDO_ENV,
):
    '''
    Builds a shell command with various kwargs.
    '''

    if shell_executable is None or not isinstance(shell_executable,
                                                  six.string_types):
        shell_executable = 'sh'

    if isinstance(command, six.binary_type):
        command = command.decode('utf-8')

    if env:
        env_string = ' '.join([
            '{0}={1}'.format(key, value) for key, value in six.iteritems(env)
        ])
        command = StringCommand('env', env_string, command)

    if chdir:
        command = StringCommand('cd', chdir, '&&', command)

    # Quote the command as a string
    command = QuoteString(command)

    command_bits = []

    # Use sudo (w/user?)
    if use_sudo_password:
        askpass_filename, sudo_password = use_sudo_password
        command_bits.extend([
            'env',
            'SUDO_ASKPASS={0}'.format(askpass_filename),
            MaskString('{0}={1}'.format(SUDO_ASKPASS_ENV_VAR, sudo_password)),
        ])

    if sudo:
        command_bits.extend(['sudo', '-H'])

        if use_sudo_password:
            command_bits.extend(['-A', '-k'])  # use askpass, disable cache
        else:
            command_bits.append('-n')  # disable prompt/interactivity

        if use_sudo_login:
            command_bits.append('-i')

        if preserve_sudo_env:
            command_bits.append('-E')

        if sudo_user:
            command_bits.extend(('-u', sudo_user))

    # Switch user with su
    if su_user:
        command_bits.append('su')

        if use_su_login:
            show_use_su_login_warning()
            command_bits.append('-l')

        if preserve_su_env:
            command_bits.append('-m')

        if su_shell:
            command_bits.extend(['-s', '`which {0}`'.format(su_shell)])

        command_bits.extend([su_user, '-c'])

        # Quote the whole shell -c 'command' as BSD `su` does not have a shell option
        command_bits.append(
            QuoteString(StringCommand(shell_executable, '-c', command)))
    else:
        # Otherwise simply use thee shell directly
        command_bits.extend([shell_executable, '-c', command])

    return StringCommand(*command_bits)
Ejemplo n.º 6
0
def role(
    role,
    present=True,
    password=None,
    login=True,
    superuser=False,
    inherit=False,
    createdb=False,
    createrole=False,
    replication=False,
    connection_limit=None,
    # Details for speaking to PostgreSQL via `psql` CLI
    psql_user=None,
    psql_password=None,
    psql_host=None,
    psql_port=None,
):
    """
    Add/remove PostgreSQL roles.

    + role: name of the role
    + present: whether the role should be present or absent
    + password: the password for the role
    + login: whether the role can login
    + superuser: whether role will be a superuser
    + inherit: whether the role inherits from other roles
    + createdb: whether the role is allowed to create databases
    + createrole: whether the role is allowed to create new roles
    + replication: whether this role is allowed to replicate
    + connection_limit: the connection limit for the role
    + psql_*: global module arguments, see above

    Updates:
        pyinfra will not attempt to change existing roles - it will either
        create or drop roles, but not alter them (if the role exists this
        operation will make no changes).

    **Example:**

    .. code:: python

        postgresql.role(
            name="Create the pyinfra PostgreSQL role",
            role="pyinfra",
            password="******",
            superuser=True,
            login=True,
            sudo_user="******",
        )

    """

    roles = host.get_fact(
        PostgresqlRoles,
        psql_user=psql_user,
        psql_password=psql_password,
        psql_host=psql_host,
        psql_port=psql_port,
    )

    is_present = role in roles

    # User not wanted?
    if not present:
        if is_present:
            yield make_execute_psql_command(
                'DROP ROLE "{0}"'.format(role),
                user=psql_user,
                password=psql_password,
                host=psql_host,
                port=psql_port,
            )
            roles.pop(role)
        else:
            host.noop("postgresql role {0} does not exist".format(role))
        return

    # If we want the user and they don't exist
    if not is_present:
        sql_bits = ['CREATE ROLE "{0}"'.format(role)]

        for key, value in (
            ("LOGIN", login),
            ("SUPERUSER", superuser),
            ("INHERIT", inherit),
            ("CREATEDB", createdb),
            ("CREATEROLE", createrole),
            ("REPLICATION", replication),
        ):
            if value:
                sql_bits.append(key)

        if connection_limit:
            sql_bits.append("CONNECTION LIMIT {0}".format(connection_limit))

        if password:
            sql_bits.append(MaskString("PASSWORD '{0}'".format(password)))

        yield make_execute_psql_command(
            StringCommand(*sql_bits),
            user=psql_user,
            password=psql_password,
            host=psql_host,
            port=psql_port,
        )
        roles[role] = {
            "super": superuser,
            "cretedb": createdb,
            "createrole": createrole,
        }
    else:
        host.noop("postgresql role {0} exists".format(role))
Ejemplo n.º 7
0
def user(
    user,
    # Desired user settings
    present=True,
    user_hostname='localhost',
    password=None,
    privileges=None,
    # Details for speaking to MySQL via `mysql` CLI via `mysql` CLI
    mysql_user=None,
    mysql_password=None,
    mysql_host=None,
    mysql_port=None,
    state=None,
    host=None,
):
    '''
    Add/remove/update MySQL users.

    + user: the name of the user
    + present: whether the user should exist or not
    + user_hostname: the hostname of the user
    + password: the password of the user (if created)
    + privileges: the global privileges for this user
    + mysql_*: global module arguments, see above

    Hostname:
        this + ``name`` makes the username - so changing this will create a new
        user, rather than update users with the same ``name``.

    Password:
        will only be applied if the user does not exist - ie pyinfra cannot
        detect if the current password doesn't match the one provided, so won't
        attempt to change it.

    Example:

    .. code:: python

        mysql.user(
            name='Create the pyinfra@localhost MySQL user',
            user='******',
            password='******',
        )
    '''

    current_users = host.fact.mysql_users(
        mysql_user,
        mysql_password,
        mysql_host,
        mysql_port,
    )

    user_host = '{0}@{1}'.format(user, user_hostname)
    is_present = user_host in current_users

    # User not wanted?
    if not present:
        if is_present:
            yield make_execute_mysql_command(
                'DROP USER "{0}"@"{1}"'.format(user, user_hostname),
                user=mysql_user,
                password=mysql_password,
                host=mysql_host,
                port=mysql_port,
            )
        else:
            host.noop('mysql user {0}@{1} does not exist'.format(
                user, user_hostname))
        return

    # If we want the user and they don't exist
    if present and not is_present:
        sql_bits = ['CREATE USER "{0}"@"{1}"'.format(user, user_hostname)]
        if password:
            sql_bits.append(MaskString('IDENTIFIED BY "{0}"'.format(password)))

        yield make_execute_mysql_command(
            StringCommand(*sql_bits),
            user=mysql_user,
            password=mysql_password,
            host=mysql_host,
            port=mysql_port,
        )
    else:
        host.noop('mysql user {0}@{1} exists'.format(user, user_hostname))

    # If we're here either the user exists or we just created them; either way
    # now we can check any privileges are set.
    if privileges:
        yield _privileges(
            user,
            privileges,
            user_hostname=user_hostname,
            mysql_user=mysql_user,
            mysql_password=mysql_password,
            mysql_host=mysql_host,
            mysql_port=mysql_port,
            state=state,
            host=host,
        )
Ejemplo n.º 8
0
def role(
    state, host, name,
    present=True,
    password=None, login=True, superuser=False, inherit=False,
    createdb=False, createrole=False, replication=False, connection_limit=None,
    # Details for speaking to PostgreSQL via `psql` CLI
    postgresql_user=None, postgresql_password=None,
    postgresql_host=None, postgresql_port=None,
):
    '''
    Add/remove PostgreSQL roles.

    + name: name of the role
    + present: whether the role should be present or absent
    + password: the password for the role
    + login: whether the role can login
    + superuser: whether role will be a superuser
    + inherit: whether the role inherits from other roles
    + createdb: whether the role is allowed to create databases
    + createrole: whether the role is allowed to create new roles
    + replication: whether this role is allowed to replicate
    + connection_limit: the connection limit for the role
    + postgresql_*: global module arguments, see above

    Updates:
        pyinfra will not attempt to change existing roles - it will either
        create or drop roles, but not alter them (if the role exists this
        operation will make no changes).

    Example:

    .. code:: python

        postgresql.role(
            {'Create the pyinfra PostgreSQL role'},
            'pyinfra',
            password='******',
            superuser=True,
            login=True,
            sudo_user='******',
        )

    '''

    roles = host.fact.postgresql_roles(
        postgresql_user, postgresql_password,
        postgresql_host, postgresql_port,
    )

    is_present = name in roles

    # User not wanted?
    if not present:
        if is_present:
            yield make_execute_psql_command(
                'DROP ROLE {0}'.format(name),
                user=postgresql_user,
                password=postgresql_password,
                host=postgresql_host,
                port=postgresql_port,
            )
        return

    # If we want the user and they don't exist
    if not is_present:
        sql_bits = ['CREATE ROLE {0}'.format(name)]

        for key, value in (
            ('LOGIN', login),
            ('SUPERUSER', superuser),
            ('INHERIT', inherit),
            ('CREATEDB', createdb),
            ('CREATEROLE', createrole),
            ('REPLICATION', replication),
        ):
            if value:
                sql_bits.append(key)

        if connection_limit:
            sql_bits.append('CONNECTION LIMIT {0}'.format(connection_limit))

        if password:
            sql_bits.append(MaskString("PASSWORD '{0}'".format(password)))

        yield make_execute_psql_command(
            StringCommand(*sql_bits),
            user=postgresql_user,
            password=postgresql_password,
            host=postgresql_host,
            port=postgresql_port,
        )
Ejemplo n.º 9
0
def make_unix_command(
    command,
    env=None,
    su_user=Config.SU_USER,
    use_su_login=Config.USE_SU_LOGIN,
    sudo=Config.SUDO,
    sudo_user=Config.SUDO_USER,
    use_sudo_login=Config.USE_SUDO_LOGIN,
    use_sudo_password=Config.USE_SUDO_PASSWORD,
    preserve_sudo_env=Config.PRESERVE_SUDO_ENV,
    shell_executable=Config.SHELL,
    raw=True,
):
    '''
    Builds a shell command with various kwargs.
    '''

    if shell_executable is None or not isinstance(shell_executable,
                                                  six.string_types):
        shell_executable = 'sh'

    command_bits = []

    # Use sudo (w/user?)
    if sudo:
        if use_sudo_password:
            askpass_filename, sudo_password = use_sudo_password
            command_bits.extend([
                'env',
                'SUDO_ASKPASS={0}'.format(askpass_filename),
                MaskString('{0}={1}'.format(SUDO_ASKPASS_ENV_VAR,
                                            sudo_password)),
            ])

        sudo_bits = ['sudo', '-H']

        if use_sudo_password:
            sudo_bits.extend(['-A', '-k'])  # use askpass, disable cache
        else:
            sudo_bits.append('-n')  # disable prompt/interactivity

        if use_sudo_login:
            sudo_bits.append('-i')

        if preserve_sudo_env:
            sudo_bits.append('-E')

        if sudo_user:
            sudo_bits.extend(('-u', sudo_user))

        command_bits.extend(sudo_bits)

    # Switch user with su
    if su_user:
        su_bits = ['su']

        if use_su_login:
            su_bits.append('-l')

        # note `which <shell>` usage here - su requires an absolute path
        command_bits.extend(su_bits)
        command_bits.extend(
            [su_user, '-s', '`which {0}`'.format(shell_executable), '-c'])

    else:
        # Otherwise just sh wrap the command
        command_bits.extend([shell_executable, '-c'])

    #
    # OK, now parse the command!
    #

    if isinstance(command, six.binary_type):
        command = command.decode('utf-8')

    # Use env & build our actual command
    if env:
        env_string = ' '.join([
            '{0}={1}'.format(key, value) for key, value in six.iteritems(env)
        ])
        command = StringCommand('env', env_string, command)

    # Quote the command as a string
    command = QuoteString(command)
    command_bits.append(command)

    return StringCommand(*command_bits)
Ejemplo n.º 10
0
 def test_masked(self):
     cmd = StringCommand(MaskString("adsfg"))
     assert cmd.get_raw_value() == "adsfg"
     assert str(cmd) == "***"
Ejemplo n.º 11
0
 def test_mixed_masked(self):
     cmd = StringCommand("some", "stuff", MaskString("mask me"), "other",
                         "stuff")
     assert cmd.get_raw_value() == "some stuff mask me other stuff"
     assert str(cmd) == "some stuff *** other stuff"
Ejemplo n.º 12
0
def user(
    user,
    present=True,
    user_hostname="localhost",
    password=None,
    privileges=None,
    # MySQL REQUIRE SSL/TLS options
    require=None,  # SSL or X509
    require_cipher=False,
    require_issuer=False,
    require_subject=False,
    # MySQL WITH resource limit options
    max_connections=None,
    max_queries_per_hour=None,
    max_updates_per_hour=None,
    max_connections_per_hour=None,
    # Details for speaking to MySQL via `mysql` CLI via `mysql` CLI
    mysql_user=None,
    mysql_password=None,
    mysql_host=None,
    mysql_port=None,
):
    """
    Add/remove/update MySQL users.

    + user: the name of the user
    + present: whether the user should exist or not
    + user_hostname: the hostname of the user
    + password: the password of the user (if created)
    + privileges: the global privileges for this user
    + mysql_*: global module arguments, see above

    Hostname:
        this + ``name`` makes the username - so changing this will create a new
        user, rather than update users with the same ``name``.

    Password:
        will only be applied if the user does not exist - ie pyinfra cannot
        detect if the current password doesn't match the one provided, so won't
        attempt to change it.

    **Example:**

    .. code:: python

        mysql.user(
            name="Create the pyinfra@localhost MySQL user",
            user="******",
            password="******",
        )

        # Create a user with resource limits
        mysql.user(
            name="Create the pyinfra@localhost MySQL user",
            user="******",
            max_connections=50,
            max_updates_per_hour=10,
        )

        # Create a user that requires SSL for connections
        mysql.user(
            name="Create the pyinfra@localhost MySQL user",
            user="******",
            password="******",
            require="SSL",
        )

        # Create a user that requires a specific certificate
        mysql.user(
            name="Create the pyinfra@localhost MySQL user",
            user="******",
            password="******",
            require="X509",
            require_issuer="/C=SE/ST=Stockholm...",
            require_cipher="EDH-RSA-DES-CBC3-SHA",
        )
    """

    if require and require not in ("SSL", "X509"):
        raise OperationError(
            'Invalid `require` value, must be: "SSL" or "X509"')

    if require != "X509":
        if require_cipher:
            raise OperationError(
                'Cannot set `require_cipher` if `require` is not "X509"')
        if require_issuer:
            raise OperationError(
                'Cannot set `require_issuer` if `require` is not "X509"')
        if require_subject:
            raise OperationError(
                'Cannot set `require_subject` if `require` is not "X509"')

    current_users = host.get_fact(
        MysqlUsers,
        mysql_user=mysql_user,
        mysql_password=mysql_password,
        mysql_host=mysql_host,
        mysql_port=mysql_port,
    )

    user_host = "{0}@{1}".format(user, user_hostname)
    is_present = user_host in current_users

    if not present:
        if is_present:
            yield make_execute_mysql_command(
                'DROP USER "{0}"@"{1}"'.format(user, user_hostname),
                user=mysql_user,
                password=mysql_password,
                host=mysql_host,
                port=mysql_port,
            )
            current_users.pop(user_host)
        else:
            host.noop("mysql user {0}@{1} does not exist".format(
                user, user_hostname))
        return

    new_or_updated_user_fact = {
        "ssl_type": "ANY" if require == "SSL" else require,
        "ssl_cipher": require_cipher,
        "x509_issuer": require_issuer,
        "x509_subject": require_subject,
        "max_user_connections": max_connections,
        "max_questions": max_queries_per_hour,
        "max_updates": max_updates_per_hour,
        "max_connections": max_connections_per_hour,
    }

    if present and not is_present:
        sql_bits = ['CREATE USER "{0}"@"{1}"'.format(user, user_hostname)]
        if password:
            sql_bits.append(MaskString('IDENTIFIED BY "{0}"'.format(password)))

        if require == "SSL":
            sql_bits.append("REQUIRE SSL")

        if require == "X509":
            sql_bits.append("REQUIRE")
            require_bits = []

            if require_cipher:
                require_bits.append('CIPHER "{0}"'.format(require_cipher))
            if require_issuer:
                require_bits.append('ISSUER "{0}"'.format(require_issuer))
            if require_subject:
                require_bits.append('SUBJECT "{0}"'.format(require_subject))

            if not require_bits:
                require_bits.append("X509")

            sql_bits.extend(require_bits)

        resource_bits = []
        if max_connections:
            resource_bits.append(
                "MAX_USER_CONNECTIONS {0}".format(max_connections))
        if max_queries_per_hour:
            resource_bits.append(
                "MAX_QUERIES_PER_HOUR {0}".format(max_queries_per_hour))
        if max_updates_per_hour:
            resource_bits.append(
                "MAX_UPDATES_PER_HOUR {0}".format(max_updates_per_hour))
        if max_connections_per_hour:
            resource_bits.append("MAX_CONNECTIONS_PER_HOUR {0}".format(
                max_connections_per_hour))

        if resource_bits:
            sql_bits.append("WITH")
            sql_bits.append(" ".join(resource_bits))

        yield make_execute_mysql_command(
            StringCommand(*sql_bits),
            user=mysql_user,
            password=mysql_password,
            host=mysql_host,
            port=mysql_port,
        )

        current_users[user_host] = new_or_updated_user_fact

    if present and is_present:
        current_user = current_users.get(user_host)

        alter_bits = []

        if require == "SSL":
            if current_user["ssl_type"] != "ANY":
                alter_bits.append("REQUIRE SSL")

        if require == "X509":
            require_bits = []

            if require_cipher and current_user["ssl_cipher"] != require_cipher:
                require_bits.append('CIPHER "{0}"'.format(require_cipher))
            if require_issuer and current_user["x509_issuer"] != require_issuer:
                require_bits.append('ISSUER "{0}"'.format(require_issuer))
            if require_subject and current_user[
                    "x509_subject"] != require_subject:
                require_bits.append('SUBJECT "{0}"'.format(require_subject))

            if not require_bits:
                if current_user["ssl_type"] != "X509":
                    require_bits.append("X509")

            if require_bits:
                alter_bits.append("REQUIRE")
                alter_bits.extend(require_bits)

        resource_bits = []
        if max_connections and current_user[
                "max_user_connections"] != max_connections:
            resource_bits.append(
                "MAX_USER_CONNECTIONS {0}".format(max_connections))
        if max_queries_per_hour and current_user[
                "max_questions"] != max_queries_per_hour:
            resource_bits.append(
                "MAX_QUERIES_PER_HOUR {0}".format(max_queries_per_hour))
        if max_updates_per_hour and current_user[
                "max_updates"] != max_updates_per_hour:
            resource_bits.append(
                "MAX_UPDATES_PER_HOUR {0}".format(max_updates_per_hour))
        if max_connections_per_hour and current_user[
                "max_connections"] != max_connections_per_hour:
            resource_bits.append("MAX_CONNECTIONS_PER_HOUR {0}".format(
                max_connections_per_hour))

        if resource_bits:
            alter_bits.append("WITH")
            alter_bits.append(" ".join(resource_bits))

        if alter_bits:
            sql_bits = ['ALTER USER "{0}"@"{1}"'.format(user, user_hostname)]
            sql_bits.extend(alter_bits)
            yield make_execute_mysql_command(
                StringCommand(*sql_bits),
                user=mysql_user,
                password=mysql_password,
                host=mysql_host,
                port=mysql_port,
            )
            current_user.update(new_or_updated_user_fact)
        else:
            host.noop("mysql user {0}@{1} exists".format(user, user_hostname))

    # If we're here either the user exists or we just created them; either way
    # now we can check any privileges are set.
    if privileges:
        yield from _privileges(
            user,
            privileges,
            user_hostname=user_hostname,
            mysql_user=mysql_user,
            mysql_password=mysql_password,
            mysql_host=mysql_host,
            mysql_port=mysql_port,
        )
Ejemplo n.º 13
0
 def test_mixed_masked(self):
     cmd = StringCommand('some', 'stuff', MaskString('mask me'), 'other',
                         'stuff')
     assert cmd.get_raw_value() == 'some stuff mask me other stuff'
     assert str(cmd) == 'some stuff *** other stuff'
Ejemplo n.º 14
0
 def test_masked(self):
     cmd = StringCommand(MaskString('adsfg'))
     assert cmd.get_raw_value() == 'adsfg'
     assert str(cmd) == '***'
Ejemplo n.º 15
0
    def test_one(self):
        ts = StringCommand(MaskString('adsfg'))

        assert ts.get_raw_value() == 'adsfg'
        assert str(ts) == '***'
Ejemplo n.º 16
0
    def test_two(self):
        ts = StringCommand('some', 'stuff', MaskString('mask me'), 'other',
                           'stuff')

        assert ts.get_raw_value() == 'some stuff mask me other stuff'
        assert str(ts) == 'some stuff *** other stuff'