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, )
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)
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), )
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)
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)
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))
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, )
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, )
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)
def test_masked(self): cmd = StringCommand(MaskString("adsfg")) assert cmd.get_raw_value() == "adsfg" assert str(cmd) == "***"
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"
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, )
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'
def test_masked(self): cmd = StringCommand(MaskString('adsfg')) assert cmd.get_raw_value() == 'adsfg' assert str(cmd) == '***'
def test_one(self): ts = StringCommand(MaskString('adsfg')) assert ts.get_raw_value() == 'adsfg' assert str(ts) == '***'
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'