Merge pull request 'Release 10.4.0' (#121) from unstable into stable
Reviewed-on: #121
This commit is contained in:
commit
6e7acd1abd
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -20,6 +20,19 @@ The **patch** part changes incrementally at each release.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
## [10.4.0] 2020-12-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* certbot: detect domains if missing
|
||||||
|
* certbot: new "sync_remote.sh" hook to sync certificates and execute hooks on remote servers
|
||||||
|
* varnish: variable for jail configuration
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* certbot: disable auth for Let's Encrypt challenge
|
||||||
|
* nginx: change from "nginx_status-XXX" to "server-status-XXX"
|
||||||
|
|
||||||
## [10.3.0] 2020-12-21
|
## [10.3.0] 2020-12-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
error() {
|
||||||
|
>&2 echo "${PROGNAME}: $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
debug() {
|
||||||
|
if [ "${VERBOSE}" = "1" ] && [ "${QUIET}" != "1" ]; then
|
||||||
|
>&2 echo "${PROGNAME}: $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
found_renewed_lineage() {
|
||||||
|
test -f "${RENEWED_LINEAGE}/fullchain.pem" && test -f "${RENEWED_LINEAGE}/privkey.pem"
|
||||||
|
}
|
||||||
|
domain_from_cert() {
|
||||||
|
openssl x509 -noout -subject -in "${RENEWED_LINEAGE}/fullchain.pem" | sed 's/^.*CN\ *=\ *//'
|
||||||
|
}
|
||||||
|
main() {
|
||||||
|
if [ -z "${RENEWED_LINEAGE}" ]; then
|
||||||
|
error "Missing RENEWED_LINEAGE environment variable (usually provided by certbot)."
|
||||||
|
fi
|
||||||
|
if [ -z "${servers}" ]; then
|
||||||
|
debug "Empty server list, skip."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if found_renewed_lineage; then
|
||||||
|
RENEWED_DOMAINS=${RENEWED_DOMAINS:-$(domain_from_cert)}
|
||||||
|
|
||||||
|
remore_lineage=${remote_dir}/renewed_lineage/$(basename ${RENEWED_LINEAGE})
|
||||||
|
|
||||||
|
for server in ${servers}; do
|
||||||
|
remote_host="root@${server}"
|
||||||
|
ssh ${remote_host} "mkdir -p ${remote_dir}" \
|
||||||
|
|| error "Couldn't create ${remote_dir} directory ${server}"
|
||||||
|
|
||||||
|
rsync --archive --copy-links --delete ${RENEWED_LINEAGE}/ ${remote_host}:${remore_lineage}/ \
|
||||||
|
|| error "Couldn't sync certificate on ${server}"
|
||||||
|
|
||||||
|
rsync --archive --copy-links --delete --exclude $0 --delete-excluded ${hooks_dir}/ ${remote_host}:${remote_dir}/hooks/ \
|
||||||
|
|| error "Couldn't sync hooks on ${server}"
|
||||||
|
|
||||||
|
ssh ${remote_host} "export RENEWED_LINEAGE=\"${remore_lineage}/\" RENEWED_DOMAINS=${RENEWED_DOMAINS}; find ${remote_dir}/hooks/ -mindepth 1 -maxdepth 1 -type f -executable -exec {} \;" \
|
||||||
|
|| error "Something went wrong on ${server} for deploy hooks"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
error "Couldn't find required files in \`${RENEWED_LINEAGE}'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly PROGNAME=$(basename "$0")
|
||||||
|
readonly VERBOSE=${VERBOSE:-"0"}
|
||||||
|
readonly QUIET=${QUIET:-"0"}
|
||||||
|
|
||||||
|
readonly hooks_dir="/etc/letsencrypt/renewal-hooks/deploy"
|
||||||
|
readonly remote_dir="/root/cert_sync"
|
||||||
|
|
||||||
|
readonly servers=""
|
||||||
|
|
||||||
|
main
|
|
@ -9,6 +9,13 @@ debug() {
|
||||||
>&2 echo "${PROGNAME}: $1"
|
>&2 echo "${PROGNAME}: $1"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
domain_from_cert() {
|
||||||
|
if [ -f "${RENEWED_LINEAGE}/fullchain.pem" ]; then
|
||||||
|
openssl x509 -noout -subject -in "${RENEWED_LINEAGE}/fullchain.pem" | sed 's/^.*CN\ *=\ *//'
|
||||||
|
else
|
||||||
|
debug "Unable to find \`${RENEWED_LINEAGE}/fullchain.pem', skip domain detection."
|
||||||
|
fi
|
||||||
|
}
|
||||||
main() {
|
main() {
|
||||||
export GIT_DIR="/etc/.git"
|
export GIT_DIR="/etc/.git"
|
||||||
export GIT_WORK_TREE="/etc"
|
export GIT_WORK_TREE="/etc"
|
||||||
|
@ -17,6 +24,9 @@ main() {
|
||||||
changed_lines=$(${git_bin} status --porcelain | wc -l | tr -d ' ')
|
changed_lines=$(${git_bin} status --porcelain | wc -l | tr -d ' ')
|
||||||
|
|
||||||
if [ "${changed_lines}" != "0" ]; then
|
if [ "${changed_lines}" != "0" ]; then
|
||||||
|
if [ -z "${RENEWED_DOMAINS}" ] && [ -n "${RENEWED_LINEAGE}" ]; then
|
||||||
|
RENEWED_DOMAINS=$(domain_from_cert)
|
||||||
|
fi
|
||||||
debug "Committing for ${RENEWED_DOMAINS}"
|
debug "Committing for ${RENEWED_DOMAINS}"
|
||||||
${git_bin} add --all
|
${git_bin} add --all
|
||||||
message="[letsencrypt] certificates renewal (${RENEWED_DOMAINS})"
|
message="[letsencrypt] certificates renewal (${RENEWED_DOMAINS})"
|
||||||
|
@ -32,6 +42,5 @@ readonly VERBOSE=${VERBOSE:-"0"}
|
||||||
readonly QUIET=${QUIET:-"0"}
|
readonly QUIET=${QUIET:-"0"}
|
||||||
|
|
||||||
readonly git_bin=$(command -v git)
|
readonly git_bin=$(command -v git)
|
||||||
readonly letsencrypt_dir=/etc/letsencrypt
|
|
||||||
|
|
||||||
main
|
main
|
||||||
|
|
|
@ -5,5 +5,6 @@ location ~ /.well-known/acme-challenge {
|
||||||
alias {{ certbot_work_dir }}/;
|
alias {{ certbot_work_dir }}/;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
|
auth_basic off;
|
||||||
allow all;
|
allow all;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,6 @@ location ~ /.well-known/acme-challenge {
|
||||||
alias {{ evoacme_acme_dir }}/.well-known/acme-challenge;
|
alias {{ evoacme_acme_dir }}/.well-known/acme-challenge;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
|
auth_basic off;
|
||||||
allow all;
|
allow all;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
state: directory
|
state: directory
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- ssl
|
||||||
|
|
||||||
- name: Self-signed certificate is present in HAProxy ssl directory
|
- name: Self-signed certificate is present in HAProxy ssl directory
|
||||||
shell: "cat /etc/ssl/certs/ssl-cert-snakeoil.pem /etc/ssl/private/ssl-cert-snakeoil.key > /etc/haproxy/ssl/ssl-cert-snakeoil.pem"
|
shell: "cat /etc/ssl/certs/ssl-cert-snakeoil.pem /etc/ssl/private/ssl-cert-snakeoil.key > /etc/haproxy/ssl/ssl-cert-snakeoil.pem"
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
notify: reload haproxy
|
notify: reload haproxy
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- ssl
|
||||||
|
|
||||||
- name: HAProxy stats_access_ips are present
|
- name: HAProxy stats_access_ips are present
|
||||||
blockinfile:
|
blockinfile:
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: HAProxy stats_admin_ips are present
|
- name: HAProxy stats_admin_ips are present
|
||||||
blockinfile:
|
blockinfile:
|
||||||
|
@ -52,6 +53,7 @@
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: HAProxy maintenance_ips are present
|
- name: HAProxy maintenance_ips are present
|
||||||
blockinfile:
|
blockinfile:
|
||||||
|
@ -62,6 +64,10 @@
|
||||||
{{ ip }}
|
{{ ip }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
notify: reload haproxy
|
notify: reload haproxy
|
||||||
|
tags:
|
||||||
|
- haproxy
|
||||||
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: HAProxy deny_ips are present
|
- name: HAProxy deny_ips are present
|
||||||
blockinfile:
|
blockinfile:
|
||||||
|
@ -72,6 +78,10 @@
|
||||||
{{ ip }}
|
{{ ip }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
notify: reload haproxy
|
notify: reload haproxy
|
||||||
|
tags:
|
||||||
|
- haproxy
|
||||||
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- include: packages_backports.yml
|
- include: packages_backports.yml
|
||||||
when: haproxy_backports
|
when: haproxy_backports
|
||||||
|
@ -100,6 +110,7 @@
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: Rotate logs with dateext
|
- name: Rotate logs with dateext
|
||||||
lineinfile:
|
lineinfile:
|
||||||
|
@ -109,7 +120,7 @@
|
||||||
insertbefore: '}'
|
insertbefore: '}'
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- logrotate
|
||||||
|
|
||||||
- name: Rotate logs with nodelaycompress
|
- name: Rotate logs with nodelaycompress
|
||||||
lineinfile:
|
lineinfile:
|
||||||
|
@ -119,6 +130,6 @@
|
||||||
insertbefore: '}'
|
insertbefore: '}'
|
||||||
tags:
|
tags:
|
||||||
- haproxy
|
- haproxy
|
||||||
- config
|
- logrotate
|
||||||
|
|
||||||
- include: munin.yml
|
- include: munin.yml
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
- name: add server-status suffix in default site index if missing
|
- name: add server-status suffix in default site index if missing
|
||||||
replace:
|
replace:
|
||||||
dest: /var/www/index.html
|
dest: /var/www/index.html
|
||||||
regexp: '"/nginx_status-?"'
|
regexp: '"/server-status-?"'
|
||||||
replace: '"/nginx_status-{{ nginx_serverstatus_suffix }}"'
|
replace: '"/server-status-{{ nginx_serverstatus_suffix }}"'
|
||||||
|
|
||||||
- name: add server-status suffix in default VHost
|
- name: add server-status suffix in default VHost
|
||||||
replace:
|
replace:
|
||||||
dest: /etc/nginx/sites-available/evolinux-default.conf
|
dest: /etc/nginx/sites-available/evolinux-default.conf
|
||||||
regexp: 'location /nginx_status-? {'
|
regexp: 'location /server-status-? {'
|
||||||
replace: 'location /nginx_status-{{ nginx_serverstatus_suffix }} {'
|
replace: 'location /server-status-{{ nginx_serverstatus_suffix }} {'
|
||||||
notify: reload nginx
|
notify: reload nginx
|
||||||
|
|
|
@ -50,7 +50,7 @@ server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name munin;
|
server_name munin;
|
||||||
|
|
||||||
location /nginx_status-{{ nginx_serverstatus_suffix | mandatory }} {
|
location /server-status-{{ nginx_serverstatus_suffix | mandatory }} {
|
||||||
stub_status on;
|
stub_status on;
|
||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
[nginx_*]
|
[nginx_*]
|
||||||
env.url http://munin/nginx_status-{{ nginx_serverstatus_suffix }}
|
env.url http://munin/server-status-{{ nginx_serverstatus_suffix }}
|
||||||
|
|
|
@ -13,6 +13,7 @@ varnish_thread_pools: "{{ ansible_processor_cores * ansible_processor_count }}"
|
||||||
varnish_thread_pool_add_delay: 0
|
varnish_thread_pool_add_delay: 0
|
||||||
varnish_thread_pool_min: 500
|
varnish_thread_pool_min: 500
|
||||||
varnish_thread_pool_max: 5000
|
varnish_thread_pool_max: 5000
|
||||||
|
varnish_jail: "unix,user=vcache"
|
||||||
|
|
||||||
varnish_config_file: /etc/varnish/default.vcl
|
varnish_config_file: /etc/varnish/default.vcl
|
||||||
varnish_secret_file: /etc/varnish/secret
|
varnish_secret_file: /etc/varnish/secret
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
notify: reload varnish
|
notify: reload varnish
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- config
|
||||||
|
|
||||||
- name: Copy Custom Varnish ExecReload script (Debian <10)
|
- name: Copy Custom Varnish ExecReload script (Debian <10)
|
||||||
template:
|
template:
|
||||||
|
@ -48,6 +49,8 @@
|
||||||
- restart varnish
|
- restart varnish
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: Override Varnish systemd unit (Buster and later)
|
- name: Override Varnish systemd unit (Buster and later)
|
||||||
template:
|
template:
|
||||||
|
@ -60,17 +63,20 @@
|
||||||
- restart varnish
|
- restart varnish
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: Patch logrotate conf
|
- name: Patch logrotate conf
|
||||||
replace:
|
replace:
|
||||||
name: /etc/logrotate.d/varnish
|
name: /etc/logrotate.d/varnish
|
||||||
regexp: '^(\s+)(/usr/sbin/invoke-rc.d {{item}}.*)'
|
regexp: '^(\s+)(/usr/sbin/invoke-rc.d {{item}}.*)'
|
||||||
replace: '\1systemctl -q is-active {{item}} && \2'
|
replace: '\1systemctl -q is-active {{item}} && \2'
|
||||||
with_items:
|
loop:
|
||||||
- varnishlog
|
- varnishlog
|
||||||
- varnishncsa
|
- varnishncsa
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- logrotate
|
||||||
|
|
||||||
- name: Copy Varnish configuration
|
- name: Copy Varnish configuration
|
||||||
template:
|
template:
|
||||||
|
@ -90,6 +96,8 @@
|
||||||
notify: reload varnish
|
notify: reload varnish
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- name: Create Varnish config dir
|
- name: Create Varnish config dir
|
||||||
file:
|
file:
|
||||||
|
@ -98,6 +106,7 @@
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- config
|
||||||
|
|
||||||
- name: Copy included Varnish config
|
- name: Copy included Varnish config
|
||||||
template:
|
template:
|
||||||
|
@ -106,9 +115,11 @@
|
||||||
force: yes
|
force: yes
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
with_fileglob:
|
with_fileglob:
|
||||||
- "templates/varnish/conf.d/*.vcl"
|
- "templates/varnish/conf.d/*.vcl"
|
||||||
notify: reload varnish
|
notify: reload varnish
|
||||||
tags:
|
tags:
|
||||||
- varnish
|
- varnish
|
||||||
|
- config
|
||||||
|
- update-config
|
||||||
|
|
||||||
- include: munin.yml
|
- include: munin.yml
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=
|
ExecStart=
|
||||||
ExecStart=/usr/sbin/varnishd -j unix,user=vcache -F {{ varnish_addresses | map('regex_replace', '^(.*)$', '-a \\1') | list | join(' ') }} -T {{ varnish_management_address }} -f {{ varnish_config_file }} -S {{ varnish_secret_file }} -s {{ varnish_storage }} -p thread_pools={{ varnish_thread_pools }} -p thread_pool_add_delay={{ varnish_thread_pool_add_delay }} -p thread_pool_min={{ varnish_thread_pool_min }} -p thread_pool_max={{ varnish_thread_pool_max }}
|
ExecStart=/usr/sbin/varnishd -F -j {{ varnish_jail }} {{ varnish_addresses | map('regex_replace', '^(.*)$', '-a \\1') | list | join(' ') }} -T {{ varnish_management_address }} -f {{ varnish_config_file }} -S {{ varnish_secret_file }} -s {{ varnish_storage }} -p thread_pools={{ varnish_thread_pools }} -p thread_pool_add_delay={{ varnish_thread_pool_add_delay }} -p thread_pool_min={{ varnish_thread_pool_min }} -p thread_pool_max={{ varnish_thread_pool_max }}
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=
|
ExecStart=
|
||||||
ExecStart=/usr/sbin/varnishd -j unix,user=vcache -F {{ varnish_addresses | map('regex_replace', '^(.*)$', '-a \\1') | list | join(' ') }} -T {{ varnish_management_address }} -f {{ varnish_config_file }} -S {{ varnish_secret_file }} -s {{ varnish_storage }} -p thread_pools={{ varnish_thread_pools }} -p thread_pool_add_delay={{ varnish_thread_pool_add_delay }} -p thread_pool_min={{ varnish_thread_pool_min }} -p thread_pool_max={{ varnish_thread_pool_max }}
|
ExecStart=/usr/sbin/varnishd -F -j {{ varnish_jail }} {{ varnish_addresses | map('regex_replace', '^(.*)$', '-a \\1') | list | join(' ') }} -T {{ varnish_management_address }} -f {{ varnish_config_file }} -S {{ varnish_secret_file }} -s {{ varnish_storage }} -p thread_pools={{ varnish_thread_pools }} -p thread_pool_add_delay={{ varnish_thread_pool_add_delay }} -p thread_pool_min={{ varnish_thread_pool_min }} -p thread_pool_max={{ varnish_thread_pool_max }}
|
||||||
ExecReload=
|
ExecReload=
|
||||||
ExecReload=/etc/varnish/reload-vcl.sh
|
ExecReload=/etc/varnish/reload-vcl.sh
|
||||||
|
|
|
@ -4,36 +4,35 @@ server {
|
||||||
server_name {{ evoadminmail_host }};
|
server_name {{ evoadminmail_host }};
|
||||||
|
|
||||||
return 301 https://{{ evoadminmail_host }}$request_uri;
|
return 301 https://{{ evoadminmail_host }}$request_uri;
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
# listen [::]:80 default_server ipv6only=on; ## listen for ipv6
|
# listen [::]:80 default_server ipv6only=on; ## listen for ipv6
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/certs/{{ evoadminmail_host }}.crt;
|
ssl_certificate /etc/ssl/certs/{{ evoadminmail_host }}.crt;
|
||||||
ssl_certificate_key /etc/ssl/private/{{ evoadminmail_host }}.key;
|
ssl_certificate_key /etc/ssl/private/{{ evoadminmail_host }}.key;
|
||||||
|
|
||||||
server_name {{ evoadminmail_host }};
|
server_name {{ evoadminmail_host }};
|
||||||
index index.php;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
error_log /var/log/nginx/error.log;
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
root /usr/share/evoadmin-mail/;
|
root /usr/share/evoadmin-mail/;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.php?$args;
|
try_files $uri $uri/ /index.php?$args;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
fastcgi_pass unix:/run/php/php7.0-evoadmin-mail-fpm.sock;
|
fastcgi_pass unix:/run/php/php7.0-evoadmin-mail-fpm.sock;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /fpm-status {
|
location /fpm-status {
|
||||||
fastcgi_pass unix:/run/php/php7.0-evoadmin-mail-fpm.sock;
|
fastcgi_pass unix:/run/php/php7.0-evoadmin-mail-fpm.sock;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
allow 127.0.0.1;
|
allow 127.0.0.1;
|
||||||
|
|
Loading…
Reference in New Issue