mirror of
https://github.com/MatteZ02/infra.git
synced 2024-11-22 18:50:20 +00:00
First setup
This commit is contained in:
commit
06b582cc61
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
!/collections/.gitkeep
|
||||||
|
/collections
|
||||||
|
__pycache__
|
16
ansible.cfg
Normal file
16
ansible.cfg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[defaults]
|
||||||
|
inventory = inventory.yml
|
||||||
|
hash_behaviour = merge
|
||||||
|
gathering = smart
|
||||||
|
transport = local
|
||||||
|
display_skipped_hosts = false
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
localhost_warning = false
|
||||||
|
collections_path = collections
|
||||||
|
inject_facts_as_vars = false
|
||||||
|
force_handlers = true
|
||||||
|
action_warnings = false
|
||||||
|
inventory_unparsed_warning = false
|
||||||
|
|
||||||
|
[inventory]
|
||||||
|
host_pattern_mismatch = ignore
|
39
install.sh
Normal file
39
install.sh
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! "$BASH_VERSION" ] ; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "
|
||||||
|
==============================
|
||||||
|
|
||||||
|
Multi Platform Project - Ansible
|
||||||
|
Install Script
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
"
|
||||||
|
|
||||||
|
stop () {
|
||||||
|
|
||||||
|
echo "
|
||||||
|
==============================
|
||||||
|
"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh &> /dev/null
|
||||||
|
|
||||||
|
apt-get install -y python3-pip python3-venv jq git curl &> /dev/null
|
||||||
|
python3 -m venv /opt/ansible &> /dev/null
|
||||||
|
/opt/ansible/bin/pip3 install ansible hvac netaddr jmespath pexpect &> /dev/null
|
||||||
|
|
||||||
|
/opt/ansible/bin/ansible-galaxy collection install -r requirements.yml --upgrade &> /dev/null
|
||||||
|
|
||||||
|
/opt/ansible/bin/ansible-pull -U ssh://git@github.com/MatteZ02/mpp-ansible --private-key ~/.ssh/id_rsa tasks.yml -t install
|
||||||
|
|
||||||
|
|
||||||
|
echo "
|
||||||
|
==============================
|
||||||
|
"
|
7
inventory.yml
Normal file
7
inventory.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
all:
|
||||||
|
hosts:
|
||||||
|
localhost:
|
||||||
|
vars:
|
||||||
|
ansible_connection: local
|
||||||
|
ansible_python_interpreter: "{{ansible_playbook_python}}"
|
3
requirements.yml
Normal file
3
requirements.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
collections:
|
||||||
|
- containers.docker
|
25
tasks.yml
Normal file
25
tasks.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
- name: "Tasks"
|
||||||
|
hosts: all
|
||||||
|
module_defaults:
|
||||||
|
ansible.builtin.gather_facts:
|
||||||
|
gather_timeout: 10
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: "Install"
|
||||||
|
import_tasks: tasks/install.yml
|
||||||
|
tags:
|
||||||
|
- install
|
||||||
|
- never
|
||||||
|
|
||||||
|
- name: "Maintenance"
|
||||||
|
import_tasks: tasks/maintenance.yml
|
||||||
|
tags:
|
||||||
|
- maintenance
|
||||||
|
- never
|
||||||
|
|
||||||
|
- name: "Deployer"
|
||||||
|
import_tasks: tasks/deployer.yml
|
||||||
|
tags:
|
||||||
|
- deployer
|
||||||
|
- never
|
217
tasks/deployer.yml
Normal file
217
tasks/deployer.yml
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
---
|
||||||
|
- name: "Deployer - SSH - Generate Keypairs"
|
||||||
|
community.crypto.openssh_keypair:
|
||||||
|
path: "/root/.ssh/keys/{{ path }}"
|
||||||
|
type: rsa
|
||||||
|
comment: "{{ ansible_facts.fqdn }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0600'
|
||||||
|
loop: "{{ paths[ansible_facts.fqdn] }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ path }}"
|
||||||
|
loop_var: "path"
|
||||||
|
vars:
|
||||||
|
paths:
|
||||||
|
http://divarinet.northeurope.cloudapp.azure.com:
|
||||||
|
- github-MetroHege-MPP-Frontend
|
||||||
|
- github-metrohege-MPP-Backend
|
||||||
|
tags:
|
||||||
|
- ssh
|
||||||
|
|
||||||
|
- name: "Deployer - Certbot - Renew Certificates"
|
||||||
|
command: "certbot renew"
|
||||||
|
register: task
|
||||||
|
changed_when: task.stdout.find("No renewals were attempted.") == -1
|
||||||
|
tags:
|
||||||
|
- certbot
|
||||||
|
- tls
|
||||||
|
|
||||||
|
- name: "Deployer - Certbot - Copy Certificates"
|
||||||
|
copy:
|
||||||
|
src: "/etc/letsencrypt/live/mpp/"
|
||||||
|
dest: "/root/certs/mpp/"
|
||||||
|
follow: true
|
||||||
|
register: task
|
||||||
|
tags:
|
||||||
|
- certbot
|
||||||
|
- tls
|
||||||
|
|
||||||
|
- name: "Deployer - MariaDB - Pull Image"
|
||||||
|
containers.podman.podman_image:
|
||||||
|
name: docker.io/mariadb
|
||||||
|
tag: latest
|
||||||
|
register: deployerTask101
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
|
||||||
|
- name: "Deployer - MariaDB - Run Container"
|
||||||
|
containers.podman.podman_container:
|
||||||
|
name: mariadb
|
||||||
|
image: docker.io/mariadb:latest
|
||||||
|
state: started
|
||||||
|
restart: on
|
||||||
|
network: host
|
||||||
|
volumes:
|
||||||
|
- "/root/mariadb:/var/lib/mysql"
|
||||||
|
restart_policy: always
|
||||||
|
env:
|
||||||
|
# TODO
|
||||||
|
MYSQL_ROOT_PASSWORD: "no password?"
|
||||||
|
register: deployerTask102
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask101 is defined and deployerTask101.changed) or deployerTask101 is undefined
|
||||||
|
tags:
|
||||||
|
- mariadb
|
||||||
|
|
||||||
|
- name: "Deployer - MariaDB - Wait"
|
||||||
|
wait_for:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: "3306"
|
||||||
|
delay: 10
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask102 is defined and deployerTask102.changed) or deployerTask102 is undefined
|
||||||
|
tags:
|
||||||
|
- mariadb
|
||||||
|
|
||||||
|
- name: "Deployer - MariaDB - Upgrade"
|
||||||
|
containers.podman.podman_container_exec:
|
||||||
|
container: "mariadb"
|
||||||
|
command: "mariadb-upgrade --host=127.0.0.1 --user=root --password=wE7qVL67xJaaXkfo"
|
||||||
|
register: task
|
||||||
|
ignore_errors: yes
|
||||||
|
changed_when: task.stdout.find("This installation of MariaDB is already upgraded") == -1
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask102 is defined and deployerTask102.changed) or deployerTask102 is undefined
|
||||||
|
tags:
|
||||||
|
- mariadb
|
||||||
|
|
||||||
|
- name: "Deployer - MariaDB - Create Users"
|
||||||
|
mysql_user:
|
||||||
|
login_host: "127.0.0.1"
|
||||||
|
login_user: root
|
||||||
|
login_password: "wE7qVL67xJaaXkfo"
|
||||||
|
ca_cert: "/etc/letsencrypt/live/mpp/chain.pem"
|
||||||
|
check_hostname: no
|
||||||
|
name: "mpp"
|
||||||
|
host: "%"
|
||||||
|
password: "UOzw4ijLaJQI13Ec"
|
||||||
|
priv: "mpp.*:ALL"
|
||||||
|
vars:
|
||||||
|
ansible_python_interpreter: "/opt/ansible/bin/python3"
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask102 is defined and deployerTask102.changed) or deployerTask102 is undefined
|
||||||
|
tags:
|
||||||
|
- mariadb
|
||||||
|
|
||||||
|
- name: "Deployer - MariaDB - Create Database"
|
||||||
|
mysql_db:
|
||||||
|
login_host: "127.0.0.1"
|
||||||
|
login_user: "mpp"
|
||||||
|
login_password: "UOzw4ijLaJQI13Ec"
|
||||||
|
ca_cert: "/etc/letsencrypt/live/mpp/chain.pem"
|
||||||
|
check_hostname: no
|
||||||
|
name: "mpp"
|
||||||
|
vars:
|
||||||
|
ansible_python_interpreter: "/opt/ansible/bin/python3"
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask102 is defined and deployerTask102.changed) or deployerTask102 is undefined
|
||||||
|
tags:
|
||||||
|
- mariadb
|
||||||
|
|
||||||
|
- name: "Deployer - Backend - Git Operations"
|
||||||
|
git:
|
||||||
|
repo: git@github.com:MetroHege/MPP-Frontend.git
|
||||||
|
dest: /root/backend
|
||||||
|
version: master
|
||||||
|
register: deployerTask521
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
|
||||||
|
- name: "Deployer - Backend - Build Image"
|
||||||
|
containers.podman.podman_image:
|
||||||
|
name: mpp/backend
|
||||||
|
tag: latest
|
||||||
|
path: /root/backend
|
||||||
|
build:
|
||||||
|
file: Dockerfile
|
||||||
|
format: docker
|
||||||
|
cache: on
|
||||||
|
force: on
|
||||||
|
become: on
|
||||||
|
become_user: root
|
||||||
|
register: deployerTask522
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask521 is defined and deployerTask521.changed) or deployerTask521 is undefined
|
||||||
|
tags:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
- name: "Deployer -Backend - Run Container"
|
||||||
|
containers.podman.podman_container:
|
||||||
|
name: backend
|
||||||
|
image: mpp/backend:latest
|
||||||
|
state: started
|
||||||
|
recreate: on
|
||||||
|
network: host
|
||||||
|
volumes:
|
||||||
|
- "/root/backend/.env:/usr/src/app/.env:ro"
|
||||||
|
restart_policy: always
|
||||||
|
command: "npm start"
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- deployerTask522 is defined
|
||||||
|
- deployerTask522.changed
|
||||||
|
tags:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
- name: "Deployer - Frontend - Git Operations"
|
||||||
|
git:
|
||||||
|
repo: git@github.com:MetroHege/MPP-Frontend.git
|
||||||
|
dest: /root/backend/client
|
||||||
|
version: master
|
||||||
|
register: deployerTask531
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
|
||||||
|
- name: "Deployer - Frontend - Build Image"
|
||||||
|
containers.podman.podman_image:
|
||||||
|
name: mpp/frontend
|
||||||
|
tag: latest
|
||||||
|
path: /root/backend/client
|
||||||
|
build:
|
||||||
|
file: Dockerfile
|
||||||
|
format: docker
|
||||||
|
cache: on
|
||||||
|
force: on
|
||||||
|
become: on
|
||||||
|
become_user: root
|
||||||
|
register: deployerTask532
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- (deployerTask531 is defined and deployerTask531.changed) or deployerTask531 is undefined
|
||||||
|
tags:
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
- name: "Deployer - Frontend - Run Container"
|
||||||
|
containers.podman.podman_container:
|
||||||
|
name: frontend
|
||||||
|
image: mpp/frontend:latest
|
||||||
|
state: started
|
||||||
|
recreate: on
|
||||||
|
network: host
|
||||||
|
volumes:
|
||||||
|
- "/root/backend/client/.env:/usr/src/app/.env:ro"
|
||||||
|
restart_policy: always
|
||||||
|
command: "npm start"
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
- deployerTask532 is defined
|
||||||
|
- deployerTask532.changed
|
||||||
|
tags:
|
||||||
|
- frontend
|
163
tasks/install.yml
Normal file
163
tasks/install.yml
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
- name: "Install - Ansible - Python Library"
|
||||||
|
pip:
|
||||||
|
name: ansible
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
tags:
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
- name: "Install - Ansible - Create Symbolic Links"
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: /opt/ansible/bin/{{ binary }}
|
||||||
|
dest: /usr/local/bin/{{ binary }}
|
||||||
|
state: link
|
||||||
|
vars:
|
||||||
|
binaries:
|
||||||
|
- ansible
|
||||||
|
- ansible-community
|
||||||
|
- ansible-config
|
||||||
|
- ansible-connection
|
||||||
|
- ansible-console
|
||||||
|
- ansible-doc
|
||||||
|
- ansible-galaxy
|
||||||
|
- ansible-inventory
|
||||||
|
- ansible-playbook
|
||||||
|
- ansible-pull
|
||||||
|
- ansible-test
|
||||||
|
- ansible-vault
|
||||||
|
loop: "{{ binaries }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ binary }}"
|
||||||
|
loop_var: "binary"
|
||||||
|
tags:
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
- name: "Install - Ansible - Dependencies / Python Library : hvac"
|
||||||
|
pip:
|
||||||
|
name: hvac
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
tags:
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
- name: "Install - Ansible - Dependencies / Python Library : netaddr"
|
||||||
|
pip:
|
||||||
|
name: netaddr
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
tags:
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
- name: "Install - Ansible - Dependencies / Python Library : jmespath"
|
||||||
|
pip:
|
||||||
|
name: jmespath
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
tags:
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
- name: "Install - Ansible - Dependencies / Python Library : pexpect"
|
||||||
|
pip:
|
||||||
|
name: pexpect
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
tags:
|
||||||
|
- ansible
|
||||||
|
|
||||||
|
- name: "Install - Podman"
|
||||||
|
apt:
|
||||||
|
name: podman
|
||||||
|
state: latest
|
||||||
|
tags:
|
||||||
|
- podman
|
||||||
|
|
||||||
|
- name: "Install - Certbot - Python Library"
|
||||||
|
pip:
|
||||||
|
name: certbot
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
tags:
|
||||||
|
- certbot
|
||||||
|
|
||||||
|
- name: "Install - Certbot - Create Symbolic Links"
|
||||||
|
ansible.builtin.file:
|
||||||
|
src: /opt/ansible/bin/{{ binary }}
|
||||||
|
dest: /usr/local/bin/{{ binary }}
|
||||||
|
state: link
|
||||||
|
vars:
|
||||||
|
binaries:
|
||||||
|
- certbot
|
||||||
|
loop: "{{ binaries }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ binary }}"
|
||||||
|
loop_var: "binary"
|
||||||
|
when:
|
||||||
|
- ansible_facts.fqdn == "http://divarinet.northeurope.cloudapp.azure.com"
|
||||||
|
tags:
|
||||||
|
- certbot
|
||||||
|
|
||||||
|
- name: "Install - Certbot - Auth Hook"
|
||||||
|
get_url:
|
||||||
|
url: "https://git.waren.io/warengroup/acme-dns-auth/raw/branch/master/acme-dns-auth.py"
|
||||||
|
dest: "/etc/letsencrypt/renewal-hooks/pre/acme-dns-auth.py"
|
||||||
|
mode: '700'
|
||||||
|
force: true
|
||||||
|
tags:
|
||||||
|
- certbot
|
||||||
|
|
||||||
|
- name: "Install - Certbot - Create Certificates"
|
||||||
|
command: "certbot certonly --cert-name {{ cert.name }} --manual --preferred-challenges dns-01 --email {{ cert.email }} --server https://acme-v02.api.letsencrypt.org/directory --agree-tos -n --manual-auth-hook /etc/letsencrypt/renewal-hooks/pre/acme-dns-auth.py --debug-challenges --preferred-chain='ISRG Root X1' --key-type rsa -d {{ cert.domains | join(' -d ') }}"
|
||||||
|
register: task
|
||||||
|
changed_when: task.stdout.find("Certificate not yet due for renewal; no action taken.") == -1
|
||||||
|
vars:
|
||||||
|
cert:
|
||||||
|
name: musix
|
||||||
|
email: musixdiscordbot@gmail.com
|
||||||
|
domains:
|
||||||
|
- musix-org.com
|
||||||
|
- "*.musix-org.com"
|
||||||
|
tags:
|
||||||
|
- certbot
|
||||||
|
|
||||||
|
- name: "Install - MariaDB - Dependencies / Python Library : pymysql"
|
||||||
|
pip:
|
||||||
|
name: pymysql
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
|
||||||
|
- name: "Install - MariaDB - Dependencies / Package : mariadb-client"
|
||||||
|
apt:
|
||||||
|
name: "mariadb-client"
|
||||||
|
state: latest
|
||||||
|
when:
|
||||||
|
- ansible_facts.distribution == "Debian" or ansible_facts.distribution == "Ubuntu" or ansible_facts.distribution == "Linux Mint"
|
||||||
|
|
||||||
|
- name: "Install - Schedule - Maintenance"
|
||||||
|
cron:
|
||||||
|
name: Maintenance
|
||||||
|
hour: "*/3"
|
||||||
|
minute: "0"
|
||||||
|
job: "/opt/ansible/bin/ansible-pull -U ssh://git@github.com/musix-org/ansible --accept-host-key --private-key ~/.ssh/id_rsa tasks.yml -t maintenance"
|
||||||
|
|
||||||
|
- name: "Install - Schedule - Deployer"
|
||||||
|
cron:
|
||||||
|
name: Deployer
|
||||||
|
minute: "*/5"
|
||||||
|
job: "/opt/ansible/bin/ansible-pull -U ssh://git@github.com/musix-org/ansible --accept-host-key --private-key ~/.ssh/id_rsa tasks.yml -t deployer"
|
37
tasks/maintenance.yml
Normal file
37
tasks/maintenance.yml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
- name: "Maintenance - OS Update"
|
||||||
|
apt:
|
||||||
|
upgrade: dist
|
||||||
|
update_cache: yes
|
||||||
|
|
||||||
|
- name: "Maintenance - Ansible : Dependencies - Python Library : hvac"
|
||||||
|
pip:
|
||||||
|
name: hvac
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
|
||||||
|
- name: "Maintenance - Ansible : Dependencies - Python Library : netaddr"
|
||||||
|
pip:
|
||||||
|
name: netaddr
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
|
||||||
|
- name: "Maintenance - Ansible : Dependencies - Python Library : jmespath"
|
||||||
|
pip:
|
||||||
|
name: jmespath
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
||||||
|
|
||||||
|
- name: "Maintenance - Ansible : Dependencies - Python Library : pexpect"
|
||||||
|
pip:
|
||||||
|
name: pexpect
|
||||||
|
state: latest
|
||||||
|
extra_args: --upgrade
|
||||||
|
virtualenv: /opt/ansible
|
||||||
|
virtualenv_command: "python3 -m venv"
|
Loading…
Reference in New Issue
Block a user