feat(prometheus): basic setup based on template-formula
This commit is contained in:
parent
88d3f3e2f8
commit
b9b7cc0841
64
pillar.example
Normal file
64
pillar.example
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=yaml
|
||||||
|
---
|
||||||
|
prometheus:
|
||||||
|
pkg: prometheus
|
||||||
|
config_file: /etc/prometheus/prometheus.yml
|
||||||
|
service:
|
||||||
|
name: prometheus
|
||||||
|
flags: --web.listen-address="0.0.0.0:9090"
|
||||||
|
|
||||||
|
tofs:
|
||||||
|
# The files_switch key serves as a selector for alternative
|
||||||
|
# directories under the formula files directory. See TOFS pattern
|
||||||
|
# doc for more info.
|
||||||
|
# Note: Any value not evaluated by `config.get` will be used literally.
|
||||||
|
# This can be used to set custom paths, as many levels deep as required.
|
||||||
|
files_switch:
|
||||||
|
- any/path/can/be/used/here
|
||||||
|
- id
|
||||||
|
- osfinger
|
||||||
|
- os
|
||||||
|
- os_family
|
||||||
|
# All aspects of path/file resolution are customisable using the options below.
|
||||||
|
# This is unnecessary in most cases; there are sensible defaults.
|
||||||
|
# path_prefix: prometheus_alt
|
||||||
|
# dirs:
|
||||||
|
# files: files_alt
|
||||||
|
# default: default_alt
|
||||||
|
# source_files:
|
||||||
|
# prometheus-config-file-file-managed:
|
||||||
|
# - 'example_alt.tmpl'
|
||||||
|
# - 'example_alt.tmpl.jinja'
|
||||||
|
|
||||||
|
# Pillar-based config
|
||||||
|
config:
|
||||||
|
# my global config
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
|
||||||
|
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
|
||||||
|
# scrape_timeout is set to the global default (10s).
|
||||||
|
|
||||||
|
# Alertmanager configuration
|
||||||
|
alerting:
|
||||||
|
alertmanagers:
|
||||||
|
- static_configs:
|
||||||
|
- targets:
|
||||||
|
# - alertmanager:9093
|
||||||
|
|
||||||
|
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
|
||||||
|
rule_files:
|
||||||
|
# - "first_rules.yml"
|
||||||
|
# - "second_rules.yml"
|
||||||
|
|
||||||
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
|
# Here it's Prometheus itself.
|
||||||
|
scrape_configs:
|
||||||
|
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
|
||||||
|
# metrics_path defaults to '/metrics'
|
||||||
|
# scheme defaults to 'http'.
|
||||||
|
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
7
prometheus/clean.sls
Normal file
7
prometheus/clean.sls
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
include:
|
||||||
|
- .service.clean
|
||||||
|
- .config.clean
|
||||||
|
- .package.clean
|
16
prometheus/config/clean.sls
Normal file
16
prometheus/config/clean.sls
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- set sls_service_clean = tplroot ~ '.service.clean' %}
|
||||||
|
{%- from tplroot ~ "/map.jinja" import prometheus with context %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- {{ sls_service_clean }}
|
||||||
|
|
||||||
|
prometheus-config-clean-file-absent:
|
||||||
|
file.absent:
|
||||||
|
- name: {{ prometheus.config_file }}
|
||||||
|
- require:
|
||||||
|
- sls: {{ sls_service_clean }}
|
27
prometheus/config/file.sls
Normal file
27
prometheus/config/file.sls
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- set sls_package_install = tplroot ~ '.package.install' %}
|
||||||
|
{%- from tplroot ~ "/map.jinja" import prometheus with context %}
|
||||||
|
{%- from tplroot ~ "/libtofs.jinja" import files_switch with context %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- {{ sls_package_install }}
|
||||||
|
|
||||||
|
prometheus-config-file-file-managed:
|
||||||
|
file.managed:
|
||||||
|
- name: {{ prometheus.config_file }}
|
||||||
|
- source: {{ files_switch(['prometheus.yml.jinja'],
|
||||||
|
lookup='prometheus-config-file-file-managed'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
- mode: 644
|
||||||
|
- user: root
|
||||||
|
- group: {{ prometheus.rootgroup }}
|
||||||
|
- template: jinja
|
||||||
|
- context:
|
||||||
|
config: {{ prometheus.config|json }}
|
||||||
|
- require:
|
||||||
|
- sls: {{ sls_package_install }}
|
5
prometheus/config/init.sls
Normal file
5
prometheus/config/init.sls
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
include:
|
||||||
|
- .file
|
11
prometheus/defaults.yaml
Normal file
11
prometheus/defaults.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=yaml
|
||||||
|
---
|
||||||
|
prometheus:
|
||||||
|
pkg: prometheus
|
||||||
|
rootgroup: root
|
||||||
|
config_file: /etc/prometheus/prometheus.yml
|
||||||
|
config: {}
|
||||||
|
service:
|
||||||
|
name: prometheus
|
||||||
|
sysrc: False
|
6
prometheus/files/default/prometheus.yml.jinja
Normal file
6
prometheus/files/default/prometheus.yml.jinja
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
########################################################################
|
||||||
|
# File managed by Salt at <{{ source }}>.
|
||||||
|
# Your changes will be overwritten.
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
{{ config|yaml(False) }}
|
7
prometheus/init.sls
Normal file
7
prometheus/init.sls
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
include:
|
||||||
|
- .package
|
||||||
|
- .config
|
||||||
|
- .service
|
100
prometheus/libtofs.jinja
Normal file
100
prometheus/libtofs.jinja
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{%- macro files_switch(source_files,
|
||||||
|
lookup=None,
|
||||||
|
default_files_switch=['id', 'os_family'],
|
||||||
|
indent_width=6,
|
||||||
|
v1_path_prefix='') %}
|
||||||
|
{#-
|
||||||
|
Returns a valid value for the "source" parameter of a "file.managed"
|
||||||
|
state function. This makes easier the usage of the Template Override and
|
||||||
|
Files Switch (TOFS) pattern.
|
||||||
|
|
||||||
|
Params:
|
||||||
|
* source_files: ordered list of files to look for
|
||||||
|
* lookup: key under '<tplroot>:tofs:source_files' to override
|
||||||
|
list of source files
|
||||||
|
* default_files_switch: if there's no config (e.g. pillar)
|
||||||
|
'<tplroot>:tofs:files_switch' this is the ordered list of grains to
|
||||||
|
use as selector switch of the directories under
|
||||||
|
"<path_prefix>/files"
|
||||||
|
* indent_witdh: indentation of the result value to conform to YAML
|
||||||
|
* v1_path_prefix: (deprecated) only used for injecting a path prefix into
|
||||||
|
the source, to support older TOFS configs
|
||||||
|
|
||||||
|
Example (based on a `tplroot` of `xxx`):
|
||||||
|
|
||||||
|
If we have a state:
|
||||||
|
|
||||||
|
Deploy configuration:
|
||||||
|
file.managed:
|
||||||
|
- name: /etc/yyy/zzz.conf
|
||||||
|
- source: {{ files_switch(['/etc/yyy/zzz.conf', '/etc/yyy/zzz.conf.jinja'],
|
||||||
|
lookup='Deploy configuration'
|
||||||
|
) }}
|
||||||
|
- template: jinja
|
||||||
|
|
||||||
|
In a minion with id=theminion and os_family=RedHat, it's going to be
|
||||||
|
rendered as:
|
||||||
|
|
||||||
|
Deploy configuration:
|
||||||
|
file.managed:
|
||||||
|
- name: /etc/yyy/zzz.conf
|
||||||
|
- source:
|
||||||
|
- salt://xxx/files/theminion/etc/yyy/zzz.conf
|
||||||
|
- salt://xxx/files/theminion/etc/yyy/zzz.conf.jinja
|
||||||
|
- salt://xxx/files/RedHat/etc/yyy/zzz.conf
|
||||||
|
- salt://xxx/files/RedHat/etc/yyy/zzz.conf.jinja
|
||||||
|
- salt://xxx/files/default/etc/yyy/zzz.conf
|
||||||
|
- salt://xxx/files/default/etc/yyy/zzz.conf.jinja
|
||||||
|
- template: jinja
|
||||||
|
#}
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- set path_prefix = salt['config.get'](tplroot ~ ':tofs:path_prefix', tplroot) %}
|
||||||
|
{%- set files_dir = salt['config.get'](tplroot ~ ':tofs:dirs:files', 'files') %}
|
||||||
|
{%- set files_switch_list = salt['config.get'](
|
||||||
|
tplroot ~ ':tofs:files_switch',
|
||||||
|
default_files_switch
|
||||||
|
) %}
|
||||||
|
{#- Lookup source_files (v2), files (v1), or fallback to source_files parameter #}
|
||||||
|
{%- set src_files = salt['config.get'](
|
||||||
|
tplroot ~ ':tofs:source_files:' ~ lookup,
|
||||||
|
salt['config.get'](
|
||||||
|
tplroot ~ ':tofs:files:' ~ lookup,
|
||||||
|
source_files
|
||||||
|
)
|
||||||
|
) %}
|
||||||
|
{#- Only add to [''] when supporting older TOFS implementations #}
|
||||||
|
{%- set path_prefix_exts = [''] %}
|
||||||
|
{%- if v1_path_prefix != '' %}
|
||||||
|
{%- do path_prefix_exts.append(v1_path_prefix) %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- for path_prefix_ext in path_prefix_exts %}
|
||||||
|
{%- set path_prefix_inc_ext = path_prefix ~ path_prefix_ext %}
|
||||||
|
{#- For older TOFS implementation, use `files_switch` from the config #}
|
||||||
|
{#- Use the default, new method otherwise #}
|
||||||
|
{%- set fsl = salt['config.get'](
|
||||||
|
tplroot ~ path_prefix_ext|replace('/', ':') ~ ':files_switch',
|
||||||
|
files_switch_list
|
||||||
|
) %}
|
||||||
|
{#- Append an empty value to evaluate as `default` in the loop below #}
|
||||||
|
{%- if '' not in fsl %}
|
||||||
|
{%- do fsl.append('') %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- for fs in fsl %}
|
||||||
|
{%- for src_file in src_files %}
|
||||||
|
{%- if fs %}
|
||||||
|
{%- set fs_dir = salt['config.get'](fs, fs) %}
|
||||||
|
{%- else %}
|
||||||
|
{%- set fs_dir = salt['config.get'](tplroot ~ ':tofs:dirs:default', 'default') %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- set url = '- salt://' ~ '/'.join([
|
||||||
|
path_prefix_inc_ext,
|
||||||
|
files_dir,
|
||||||
|
fs_dir,
|
||||||
|
src_file.lstrip('/')
|
||||||
|
]) %}
|
||||||
|
{{ url | indent(indent_width, true) }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endmacro %}
|
24
prometheus/map.jinja
Normal file
24
prometheus/map.jinja
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=jinja
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{#- Start imports as #}
|
||||||
|
{%- import_yaml tplroot ~ "/defaults.yaml" as default_settings %}
|
||||||
|
{%- import_yaml tplroot ~ "/osfamilymap.yaml" as osfamilymap %}
|
||||||
|
{%- import_yaml tplroot ~ "/osmap.yaml" as osmap %}
|
||||||
|
{%- import_yaml tplroot ~ "/osfingermap.yaml" as osfingermap %}
|
||||||
|
|
||||||
|
{%- set defaults = salt['grains.filter_by'](default_settings,
|
||||||
|
default='prometheus',
|
||||||
|
merge=salt['grains.filter_by'](osfamilymap, grain='os_family',
|
||||||
|
merge=salt['grains.filter_by'](osmap, grain='os',
|
||||||
|
merge=salt['grains.filter_by'](osfingermap, grain='osfinger',
|
||||||
|
merge=salt['pillar.get']('prometheus:lookup', default={})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) %}
|
||||||
|
|
||||||
|
{#- Merge the prometheus pillar #}
|
||||||
|
{%- set prometheus = salt['pillar.get']('prometheus', default=defaults, merge=True) %}
|
40
prometheus/osfamilymap.yaml
Normal file
40
prometheus/osfamilymap.yaml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=yaml
|
||||||
|
#
|
||||||
|
# Setup variables using grains['os_family'] based logic.
|
||||||
|
# You just need to add the key:values for an `os_family` that differ
|
||||||
|
# from `defaults.yaml`.
|
||||||
|
# Only add an `os_family` which is/will be supported by the formula
|
||||||
|
#
|
||||||
|
# If you do not need to provide defaults via the `os_family` grain,
|
||||||
|
# you will need to provide at least an empty dict in this file, e.g.
|
||||||
|
# osfamilymap: {}
|
||||||
|
---
|
||||||
|
Debian: {}
|
||||||
|
|
||||||
|
RedHat: {}
|
||||||
|
|
||||||
|
Suse: {}
|
||||||
|
|
||||||
|
Gentoo: {}
|
||||||
|
|
||||||
|
Arch: {}
|
||||||
|
|
||||||
|
Alpine: {}
|
||||||
|
|
||||||
|
FreeBSD:
|
||||||
|
rootgroup: wheel
|
||||||
|
config_file: /usr/local/etc/prometheus.yml
|
||||||
|
service:
|
||||||
|
sysrc: True
|
||||||
|
|
||||||
|
OpenBSD:
|
||||||
|
rootgroup: wheel
|
||||||
|
service:
|
||||||
|
sysrc: True
|
||||||
|
|
||||||
|
Solaris: {}
|
||||||
|
|
||||||
|
Windows: {}
|
||||||
|
|
||||||
|
MacOS: {}
|
14
prometheus/osfingermap.yaml
Normal file
14
prometheus/osfingermap.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=yaml
|
||||||
|
#
|
||||||
|
# Setup variables using grains['osfinger'] based logic.
|
||||||
|
# You just need to add the key:values for an `osfinger` that differ
|
||||||
|
# from `defaults.yaml` + `os_family.yaml` + `osmap.yaml`.
|
||||||
|
# Only add an `osfinger` which is/will be supported by the formula
|
||||||
|
#
|
||||||
|
# If you do not need to provide defaults via the `os_finger` grain,
|
||||||
|
# you will need to provide at least an empty dict in this file, e.g.
|
||||||
|
# osfingermap: {}
|
||||||
|
---
|
||||||
|
# os: Ubuntu
|
||||||
|
Ubuntu-18.04: {}
|
25
prometheus/osmap.yaml
Normal file
25
prometheus/osmap.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=yaml
|
||||||
|
#
|
||||||
|
# Setup variables using grains['os'] based logic.
|
||||||
|
# You just need to add the key:values for an `os` that differ
|
||||||
|
# from `defaults.yaml` + `os_family.yaml`.
|
||||||
|
# Only add an `os` which is/will be supported by the formula
|
||||||
|
#
|
||||||
|
# If you do not need to provide defaults via the `os` grain,
|
||||||
|
# you will need to provide at least an empty dict in this file, e.g.
|
||||||
|
# osmap: {}
|
||||||
|
---
|
||||||
|
# os_family: Debian
|
||||||
|
Ubuntu: {}
|
||||||
|
|
||||||
|
Raspbian: {}
|
||||||
|
|
||||||
|
# os_family: Gentoo
|
||||||
|
Funtoo: {}
|
||||||
|
|
||||||
|
# os_family: Arch
|
||||||
|
Manjaro: {}
|
||||||
|
|
||||||
|
# os_family: Solaris
|
||||||
|
SmartOS: {}
|
16
prometheus/package/clean.sls
Normal file
16
prometheus/package/clean.sls
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- set sls_config_clean = tplroot ~ '.config.clean' %}
|
||||||
|
{%- from tplroot ~ "/map.jinja" import prometheus with context %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- {{ sls_config_clean }}
|
||||||
|
|
||||||
|
prometheus-package-clean-pkg-removed:
|
||||||
|
pkg.removed:
|
||||||
|
- name: {{ prometheus.pkg }}
|
||||||
|
- require:
|
||||||
|
- sls: {{ sls_config_clean }}
|
5
prometheus/package/init.sls
Normal file
5
prometheus/package/init.sls
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
include:
|
||||||
|
- .install
|
10
prometheus/package/install.sls
Normal file
10
prometheus/package/install.sls
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- from tplroot ~ "/map.jinja" import prometheus with context %}
|
||||||
|
|
||||||
|
prometheus-package-install-pkg-installed:
|
||||||
|
pkg.installed:
|
||||||
|
- name: {{ prometheus.pkg }}
|
18
prometheus/service/clean.sls
Normal file
18
prometheus/service/clean.sls
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- from tplroot ~ "/map.jinja" import prometheus with context %}
|
||||||
|
|
||||||
|
prometheus-service-clean-service-dead:
|
||||||
|
service.dead:
|
||||||
|
- name: {{ prometheus.service.name }}
|
||||||
|
- enable: False
|
||||||
|
|
||||||
|
{%- if prometheus.service.use_sysrc %}
|
||||||
|
prometheus_flags:
|
||||||
|
sysrc.absent:
|
||||||
|
- require:
|
||||||
|
- service: prometheus-service-clean-service-dead
|
||||||
|
{%- endif %}
|
5
prometheus/service/init.sls
Normal file
5
prometheus/service/init.sls
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
include:
|
||||||
|
- .running
|
43
prometheus/service/running.sls
Normal file
43
prometheus/service/running.sls
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: ft=sls
|
||||||
|
|
||||||
|
{#- Get the `tplroot` from `tpldir` #}
|
||||||
|
{%- set tplroot = tpldir.split('/')[0] %}
|
||||||
|
{%- set sls_config_file = tplroot ~ '.config.file' %}
|
||||||
|
{%- from tplroot ~ "/map.jinja" import prometheus with context %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- {{ sls_config_file }}
|
||||||
|
|
||||||
|
{%- if prometheus.service.sysrc %}
|
||||||
|
prometheus_args:
|
||||||
|
sysrc.managed:
|
||||||
|
- value: {{ prometheus.service.flags }}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{#- On FreeBSD restarting this service hangs. #}
|
||||||
|
{#- See https://github.com/saltstack/salt/issues/44848#issuecomment-486460601 #}
|
||||||
|
{%- if salt['grains.get']('os_family') == 'FreeBSD' %}
|
||||||
|
prometheus-service-running-service-enable:
|
||||||
|
service.enabled:
|
||||||
|
- name: {{ prometheus.service.name }}
|
||||||
|
|
||||||
|
prometheus-service-running-service-running:
|
||||||
|
cmd.run:
|
||||||
|
- name: "service {{ prometheus.service.name }} onerestart >/dev/null 2>&1"
|
||||||
|
- hide_output: True
|
||||||
|
- timeout: 60
|
||||||
|
- onchanges:
|
||||||
|
{%- else %}{# business as usual #}
|
||||||
|
prometheus-service-running-service-running:
|
||||||
|
service.running:
|
||||||
|
- name: {{ prometheus.service.name }}
|
||||||
|
- enable: True
|
||||||
|
- watch:
|
||||||
|
{%- endif %}
|
||||||
|
- file: prometheus-config-file-file-managed
|
||||||
|
{%- if prometheus.service.sysrc %}
|
||||||
|
- sysrc: prometheus_args
|
||||||
|
{%- endif %}
|
||||||
|
- require:
|
||||||
|
- sls: {{ sls_config_file }}
|
Loading…
Reference in New Issue
Block a user