#!/usr/bin/env ansible-playbook --- #==============================================================# # File : pgsql-user.yml # Desc : create or modify user/role on pgsql cluster # Ctime : 2021-02-27 # Mtime : 2022-12-29 # Path : pgsql-user.yml # Deps : templates/pg-user.sql # Docs : https://pigsty.io/docs/pgsql/playbook # License : Apache-2.0 @ https://pigsty.io/docs/about/license/ # Copyright : 2018-2026 Ruohang Feng / Vonng (rh@vonng.com) #==============================================================# #--------------------------------------------------------------# # Usage #--------------------------------------------------------------# # # 1. Define new user/role in inventory (cmdb or config) # `all.children..vars.pg_users[i]` # # 2. Execute this playbook on target cluster with arg `username` # `pgsql-user.yml -l -e username= # # This playbook will: # 1. create user sql definition on `/pg/tmp/pg-user-{{ user.name }}.sql` # 2. execute database creation/update sql on cluster leader instance # 3. update /etc/pgbouncer/userlist.txt & useropts.txt # 4. and reload pgbouncer to take effect # #--------------------------------------------------------------# # Utils #--------------------------------------------------------------# # Create pgsql user 'username' on pgsql cluster 'cls' # bin/pgsql-user # bin/pgsql-user pg-meta dbuser_meta # #--------------------------------------------------------------# # Example #--------------------------------------------------------------# # pg-meta: # vars: # pg_users: # define business users/roles on this cluster, array of user definition # - name: dbuser_meta # REQUIRED, `name` is the only mandatory field of a user definition # state: create # optional, create|absent, create by default. use 'absent' to drop user # password: DBUser.Meta # optional, password, can be a scram-sha-256 hash string or plain text # login: true # optional, can log in, true by default (new biz ROLE should be false) # superuser: false # optional, is superuser? false by default # createdb: false # optional, can create database? false by default # createrole: false # optional, can create role? false by default # inherit: true # optional, can this role use inherited privileges? true by default # replication: false # optional, can this role do replication? false by default # bypassrls: false # optional, can this role bypass row level security? false by default # pgbouncer: true # optional, add this user to pgbouncer user-list? false by default (production user should be true explicitly) # connlimit: -1 # optional, user connection limit, default -1 disable limit # expire_in: 3650 # optional, now + n days when this role is expired (OVERWRITE expire_at) # expire_at: '2030-12-31' # optional, YYYY-MM-DD 'timestamp' when this role is expired (OVERWRITTEN by expire_in) # comment: pigsty admin user # optional, comment string for this user/role # roles: [dbrole_admin] # optional, belonged roles. default roles are: dbrole_{admin,readonly,readwrite,offline} # parameters: {} # optional, role level parameters with `ALTER ROLE SET` # pool_mode: transaction # optional, pgbouncer pool mode at user level, transaction by default # pool_connlimit: -1 # optional, max database connections at user level, default -1 disable limit # search_path: public # key value config parameters according to postgresql documentation (e.g: use pigsty as default search_path) # # # User with enhanced roles syntax (PG16+ features: set/inherit options) # - name: dbuser_test # roles: # - dbrole_readwrite # simple string: GRANT role # - { name: dbrole_admin, admin: true } # grant with ADMIN OPTION (PG16: WITH ADMIN TRUE) # - { name: pg_monitor, set: false , inherit: false } # remove set/inherit option # - { name: old_role, state: absent } # REVOKE instead of GRANT # # # One-liner examples # - {name: dbuser_view ,password: DBUser.Viewer ,pgbouncer: true ,roles: [dbrole_readonly], comment: read-only viewer for meta database} # - {name: dbuser_grafana ,password: DBUser.Grafana ,pgbouncer: true ,roles: [dbrole_admin] ,comment: admin user for grafana database } # - {name: dbuser_bytebase ,password: DBUser.Bytebase ,pgbouncer: true ,roles: [dbrole_admin] ,comment: admin user for bytebase database } #--------------------------------------------------------------# - name: PGSQL USER become: true hosts: all gather_facts: no tasks: #----------------------------------------------------------# # Validate username and user definition [preflight] #----------------------------------------------------------# - name: preflight tags: [ preflight, always ] connection: local block: - name: validate username parameter assert: that: - username is defined - username != '' - username != 'postgres' fail_msg: variable 'username' should be specified (-e username=) - name: fetch user definition set_fact: user_def={{ pg_users | json_query(user_def_query) }} vars: { user_def_query: "[?name=='{{ username }}'] | [0]" } - name: validate user definition assert: that: - user_def is defined - user_def != None - user_def != '' - user_def != {} fail_msg: define user {{ username }} in pg_users first - debug: { msg: "{{ user_def }}" } #----------------------------------------------------------# # Create Postgres User [postgres] #----------------------------------------------------------# # create user according to user definition - include_tasks: roles/pgsql/tasks/user.yml tags: postgres when: pg_role == 'primary' vars: { user: "{{ user_def }}" } # write biz user plain password to .pgpass file for citus cluster - name: write plain biz user password to pgpass for citus cluster tags: postgres become_user: "{{ pg_dbsu|default('postgres') }}" when: pg_mode|default('pgsql') == 'citus' and user_def.password is defined and user_def.password != '' and not user_def.password.startswith('md5') and not user_def.password.startswith('scram') shell: /bin/bash /pg/bin/pg-pass-add "{{ user_def.name }}" "{{ user_def.password }}" args: { executable: /bin/bash } #----------------------------------------------------------# # Refresh Pgbouncer User Configuration [pgbouncer] #----------------------------------------------------------# - name: refresh pgbouncer users tags: pgbouncer when: user_def.pgbouncer is defined and user_def.pgbouncer|bool block: # regenerate user level parameters for pgbouncer if specified - name: refresh pgbouncer useropts.txt when: user_def.pool_mode is defined or user_def.pool_connlimit is defined copy: dest: /etc/pgbouncer/useropts.txt owner: "{{ pg_dbsu|default('postgres') }}" group: postgres mode: 0600 content: | # pgbouncer user options {% for user in pg_default_roles|default([]) + pg_users|default([]) %} {% if (user.state is not defined or user.state != 'absent') and ('pool_mode' in user or 'pool_connlimit' in user) %} {{ "%-27s" | format(user.name) }} = {% if 'pool_mode' in user %}pool_mode={{ user.pool_mode }}{% endif %} {% if 'pool_connlimit' in user %}max_user_connections={{ user.pool_connlimit }}{% endif %} {% endif %} {% endfor %} - name: add business users to pgbouncer ignore_errors: true environment: { PGPORT: "{{ pg_port|default(5432) }}" } become_user: "{{ pg_dbsu|default('postgres') }}" shell: /bin/bash /pg/bin/pgb-user '{{ username }}' AUTO args: { executable: /bin/bash } - name: reload pgbouncer systemd: name=pgbouncer state=reloaded enabled=yes daemon_reload=yes ...