diff --git a/README.md b/README.md index b1ebf23..8080ec1 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,46 @@ For bash based builds we support often used script-parts as predefined functions A full working example: [doc/example/50_docker_module](doc/example/50_docker_module) +## Feature AwsRdsPgMixin + +The AwsRdsPgMixin provides +* execute_pg_rds_sql - function will optionally resolve dns-c-names for trusted ssl-handshakes +* alter_db_user_password +* add_new_user +* deactivate_user + +the build.py file content: +``` +class MyBuild(..., AwsRdsPgMixin): + pass + + +@init +def initialize(project): + project.build_depends_on('ddadevops>=0.8.0') + + ... + config = add_aws_rds_pg_mixin_config(config, + stage + "-db.bcsimport.kauf." + account_name + ".breuni.de", + "kauf_bcsimport", + rds_resolve_dns=True,) + build = MyBuild(project, config) + build.initialize_build_dir() + +@task +def rotate_credentials_in(project): + build = get_devops_build(project) + build.alter_db_user_password('/postgres/support') + build.alter_db_user_password('/postgres/superuser') + build.add_new_user('/postgres/superuser', '/postgres/app', 'pg_group_role') + + +@task +def rotate_credentials_out(project): + build = get_devops_build(project) + build.deactivate_user('/postgres/superuser', 'old_user_name') +``` + # Releasing and updating ## Publish snapshot diff --git a/build.py b/build.py index 49b906f..dc44c15 100644 --- a/build.py +++ b/build.py @@ -28,7 +28,7 @@ use_plugin("python.distutils") default_task = "publish" name = "ddadevops" -version = "0.7.2.dev" +version = "0.8.0" summary = "tools to support builds combining gopass, terraform, dda-pallet, aws & hetzner-cloud" description = __doc__ authors = [Author("meissa GmbH", "buero@meissa-gmbh.de")] diff --git a/src/main/python/ddadevops/__init__.py b/src/main/python/ddadevops/__init__.py index f92ff8f..ce4ac73 100644 --- a/src/main/python/ddadevops/__init__.py +++ b/src/main/python/ddadevops/__init__.py @@ -11,6 +11,7 @@ from .devops_docker_build import DevopsDockerBuild, create_devops_docker_build_c from .hetzner_mixin import HetznerMixin, add_hetzner_mixin_config from .aws_backend_properties_mixin import AwsBackendPropertiesMixin, add_aws_backend_properties_mixin_config from .aws_mfa_mixin import AwsMfaMixin, add_aws_mfa_mixin_config +from .aws_rds_pg_mixin import AwsRdsPgMixin, add_aws_rds_pg_mixin_config from .dda_tenant_mixin import DdaTenantMixin, add_dda_tenant_mixin_config from .dda_simple_mixin import DdaSimpleMixin, add_dda_simple_mixin_config diff --git a/src/main/python/ddadevops/aws_mfa_mixin.py b/src/main/python/ddadevops/aws_mfa_mixin.py index f981cd9..e100aaa 100644 --- a/src/main/python/ddadevops/aws_mfa_mixin.py +++ b/src/main/python/ddadevops/aws_mfa_mixin.py @@ -1,15 +1,15 @@ from python_terraform import * from boto3 import * -from .credential import gopass_credential_from_env_path from .python_util import execute from .aws_backend_properties_mixin import AwsBackendPropertiesMixin def add_aws_mfa_mixin_config(config, account_id, region, -mfa_role='developer', mfa_account_prefix='', mfa_login_account_suffix='main'): + mfa_role='developer', mfa_account_prefix='', + mfa_login_account_suffix='main'): config.update({'AwsMfaMixin': {'account_id': account_id, - 'region': region, + 'region': region, 'mfa_role': mfa_role, 'mfa_account_prefix': mfa_account_prefix, 'mfa_login_account_suffix': mfa_login_account_suffix}}) @@ -38,7 +38,7 @@ class AwsMfaMixin(AwsBackendPropertiesMixin): 'mfa_account_prefix': self.mfa_account_prefix, 'mfa_login_account_suffix': self.mfa_login_account_suffix}) return ret - + def get_username_from_account(self, p_account_name): login_id = execute('cat ~/.aws/accounts | grep -A 2 "\[' + p_account_name + '\]" | grep username | awk -F= \'{print $2}\'', shell=True) diff --git a/src/main/python/ddadevops/aws_rds_pg_mixin.py b/src/main/python/ddadevops/aws_rds_pg_mixin.py new file mode 100644 index 0000000..2888ae5 --- /dev/null +++ b/src/main/python/ddadevops/aws_rds_pg_mixin.py @@ -0,0 +1,90 @@ +from .python_util import execute +from .credential import gopass_password_from_path, gopass_field_from_path +from .devops_build import DevopsBuild + + +def add_aws_rds_pg_mixin_config(config, rds_host_name, db_name, + rds_resolve_dns=False, + db_port='5432'): + config.update({'AwsRdsPgMixin': + {'rds_host_name': rds_host_name, + 'db_name': db_name, + 'rds_resolve_dns': rds_resolve_dns, + 'db_port': db_port, + }}) + return config + + +class AwsRdsPgMixin(DevopsBuild): + + def __init__(self, project, config): + super().__init__(project, config) + aws_rds_pg_mixin_config = config['AwsRdsPgMixin'] + self.rds_host_name = aws_rds_pg_mixin_config['rds_host_name'] + self.rds_resolve_dns = aws_rds_pg_mixin_config['rds_resolve_dns'] + self.db_name = aws_rds_pg_mixin_config['db_name'] + self.db_port = aws_rds_pg_mixin_config['db_port'] + + def execute_pg_rds_sql(self, user, password, sql): + if self.rds_resolve_dns: + host_cmd = "dig " + self.rds_host_name + " +short | head -n1" + host = execute(host_cmd, shell=True) + else: + host = self.rds_host_name + + cmd = "PGUSER=" + user + " PGPASSWORD=" + password + \ + " psql --dbname=" + self.db_name + " --host=" + host + " --port=" + self.db_port + \ + " --set=sslmode=require -Atc \"" + sql + "\"" + result = execute(cmd, shell=True) + print("PSQL: ", host, result.rstrip()) + return result + + def alter_db_user_password(self, gopass_path): + user_name = gopass_field_from_path( + self.gopass_stage() + gopass_path, 'user') + user_old_password = gopass_field_from_path( + self.gopass_stage() + gopass_path, 'old-password') + user_new_password = gopass_password_from_path( + self.gopass_stage() + gopass_path) + + self.execute_pg_rds_sql(user_name, user_old_password, + "ALTER ROLE " + user_name + " WITH PASSWORD '" + user_new_password + "';") + print("changed password:", self.gopass_stage(), ',', user_name) + + def add_new_user(self, gopass_path_superuser, gopass_path_new_user, group_role): + superuser_name = gopass_field_from_path( + self.gopass_stage() + gopass_path_superuser, 'user') + superuser_password = gopass_password_from_path( + self.gopass_stage() + gopass_path_superuser) + new_user_name = gopass_field_from_path( + self.gopass_stage() + gopass_path_new_user, 'user') + new_user_password = gopass_password_from_path( + self.gopass_stage() + gopass_path_new_user) + + self.execute_pg_rds_sql(superuser_name, superuser_password, + "CREATE ROLE " + new_user_name + " WITH LOGIN INHERIT PASSWORD '" + new_user_password + "';" + + "GRANT " + group_role + " TO " + new_user_name + ";") + print("created user:", self.gopass_stage(), ',', new_user_name) + + def deactivate_user(self, gopass_path_superuser, to_remove_user_name): + superuser_name = gopass_field_from_path( + self.gopass_stage() + gopass_path_superuser, 'user') + superuser_password = gopass_password_from_path( + self.gopass_stage() + gopass_path_superuser) + + owned_by_wrong_user = self.execute_pg_rds_sql(superuser_name, superuser_password, + "SELECT count(*) FROM pg_class c, pg_user u WHERE c.relowner = u.usesysid " + + "and u.usename='" + to_remove_user_name + "';") + if int(owned_by_wrong_user) > 0: + raise AssertionError( + "There are still objects owned by the user to be deleted.") + + connections = self.execute_pg_rds_sql(superuser_name, superuser_password, + "SELECT count(*) FROM pg_stat_activity WHERE application_name = " + + "'PostgreSQL JDBC Driver' AND usename = '" + to_remove_user_name + "';") + if int(connections) > 0: + raise AssertionError("User is still connected.") + + self.execute_pg_rds_sql(superuser_name, superuser_password, + "ALTER ROLE " + to_remove_user_name + " WITH NOLOGIN NOCREATEROLE;") + print('deactivated user:', to_remove_user_name)