mirror of
https://github.com/RWejlgaard/pez-infra.git
synced 2026-05-06 04:14:43 +00:00
Capture london-b media stack and systemd services (#19)
Add the full media automation stack (sonarr, radarr, prowlarr, lidarr, readarr, whisparr), media servers (jellyfin, plex), and supporting services (transmission, samba, ollama, promtail, cloudflared, vsftpd) to the repo as a media_stack Ansible role. Includes: - Custom systemd unit files for non-package-managed services - Config files for promtail, samba, transmission, vsftpd - Cron jobs for movie-rename-fix, sonarr/radarr midnight restarts - Updated deploy.yml to wire the role into london-b's stage - Updated london-b docs with full service inventory Backup script (backup.sh) already covered by the existing backup role. Node/systemd exporters already covered by existing monitoring roles. Closes PESO-92
This commit is contained in:
parent
69918c8619
commit
353c2ad790
17 changed files with 512 additions and 3 deletions
|
|
@ -54,12 +54,13 @@
|
|||
- role: caddy
|
||||
- role: status_page
|
||||
|
||||
# london-b: Docker services (storage, apps) + backups
|
||||
- name: "Stage 4b: Docker services (london-b)"
|
||||
# london-b: Docker services (storage, apps) + media stack + backups
|
||||
- name: "Stage 4b: Services (london-b)"
|
||||
hosts: london-b
|
||||
tags: [services, london-b]
|
||||
roles:
|
||||
- role: docker_services
|
||||
- role: media_stack
|
||||
- role: backup
|
||||
|
||||
# nuremberg-a: Mail (poste.io via Docker)
|
||||
|
|
|
|||
24
ansible/roles/media_stack/handlers/main.yml
Normal file
24
ansible/roles/media_stack/handlers/main.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
- name: Reload systemd daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart promtail
|
||||
ansible.builtin.systemd:
|
||||
name: promtail
|
||||
state: restarted
|
||||
|
||||
- name: Restart smbd
|
||||
ansible.builtin.systemd:
|
||||
name: smbd
|
||||
state: restarted
|
||||
|
||||
- name: Restart transmission
|
||||
ansible.builtin.systemd:
|
||||
name: transmission-daemon
|
||||
state: restarted
|
||||
|
||||
- name: Restart vsftpd
|
||||
ansible.builtin.systemd:
|
||||
name: vsftpd
|
||||
state: restarted
|
||||
128
ansible/roles/media_stack/tasks/main.yml
Normal file
128
ansible/roles/media_stack/tasks/main.yml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
# media_stack role — deploys the full media stack on london-b
|
||||
# Manages: *arr suite, jellyfin, plex, transmission, samba,
|
||||
# ollama, promtail, cloudflared, vsftpd, and cron jobs.
|
||||
|
||||
# ── Systemd service units (custom, not package-managed) ──
|
||||
|
||||
- name: Deploy custom systemd unit files
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/services/{{ item }}/{{ item }}.service"
|
||||
dest: "/etc/systemd/system/{{ item }}.service"
|
||||
mode: '0644'
|
||||
loop:
|
||||
- radarr
|
||||
- prowlarr
|
||||
- lidarr
|
||||
- readarr
|
||||
- whisparr
|
||||
- ollama
|
||||
- promtail
|
||||
notify: Reload systemd daemon
|
||||
|
||||
- name: Enable and start custom systemd services
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
enabled: true
|
||||
loop:
|
||||
- radarr
|
||||
- prowlarr
|
||||
- lidarr
|
||||
- readarr
|
||||
- ollama
|
||||
- promtail
|
||||
|
||||
# Whisparr is installed but disabled (kept as-is)
|
||||
- name: Ensure whisparr unit is present but disabled
|
||||
ansible.builtin.systemd:
|
||||
name: whisparr
|
||||
enabled: false
|
||||
|
||||
# ── Package-managed services (ensure enabled) ──
|
||||
|
||||
- name: Ensure package-managed services are enabled
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
enabled: true
|
||||
loop:
|
||||
- sonarr
|
||||
- jellyfin
|
||||
- plexmediaserver
|
||||
- transmission-daemon
|
||||
- smbd
|
||||
- vsftpd
|
||||
- cloudflared
|
||||
|
||||
# ── Configuration files ──
|
||||
|
||||
- name: Deploy promtail config
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/services/promtail/config/london-b.yml"
|
||||
dest: /etc/promtail/config.yml
|
||||
mode: '0644'
|
||||
notify: Restart promtail
|
||||
|
||||
- name: Deploy samba config
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/services/samba/config/london-b.conf"
|
||||
dest: /etc/samba/smb.conf
|
||||
mode: '0644'
|
||||
backup: true
|
||||
notify: Restart smbd
|
||||
|
||||
- name: Deploy transmission settings
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/services/transmission/config/settings.json"
|
||||
dest: /etc/transmission-daemon/settings.json
|
||||
owner: debian-transmission
|
||||
group: debian-transmission
|
||||
mode: '0600'
|
||||
notify: Restart transmission
|
||||
|
||||
- name: Deploy vsftpd config
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/services/vsftpd/config/london-b.conf"
|
||||
dest: /etc/vsftpd.conf
|
||||
mode: '0644'
|
||||
notify: Restart vsftpd
|
||||
|
||||
# ── Scripts ──
|
||||
|
||||
- name: Ensure scripts directory exists
|
||||
ansible.builtin.file:
|
||||
path: /root/scripts
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy movie-rename-fix script
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/scripts/movie-rename-fix.fish"
|
||||
dest: /root/scripts/movie-rename-fix.fish
|
||||
mode: '0755'
|
||||
|
||||
# ── Cron jobs ──
|
||||
|
||||
- name: Movie rename fix (hourly)
|
||||
ansible.builtin.cron:
|
||||
name: "Movie rename fix"
|
||||
minute: "0"
|
||||
job: "/root/scripts/movie-rename-fix.fish"
|
||||
user: root
|
||||
|
||||
- name: Restart radarr at midnight
|
||||
ansible.builtin.cron:
|
||||
name: "Restart radarr"
|
||||
minute: "0"
|
||||
hour: "0"
|
||||
job: "systemctl restart radarr"
|
||||
user: root
|
||||
|
||||
- name: Restart sonarr at midnight
|
||||
ansible.builtin.cron:
|
||||
name: "Restart sonarr"
|
||||
minute: "0"
|
||||
hour: "0"
|
||||
job: "systemctl restart sonarr"
|
||||
user: root
|
||||
7
ansible/scripts/movie-rename-fix.fish
Executable file
7
ansible/scripts/movie-rename-fix.fish
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env fish
|
||||
|
||||
cd /hdd/movies
|
||||
|
||||
for i in (find . -name "*www.UIndex.org - *")
|
||||
mv $i (echo $i | sed 's/www.UIndex.org - //')
|
||||
end
|
||||
16
ansible/services/lidarr/lidarr.service
Normal file
16
ansible/services/lidarr/lidarr.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=Lidarr Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0002
|
||||
Type=simple
|
||||
ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr/
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
14
ansible/services/ollama/ollama.service
Normal file
14
ansible/services/ollama/ollama.service
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=Ollama Service
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/ollama serve
|
||||
User=ollama
|
||||
Group=ollama
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
31
ansible/services/promtail/config/london-b.yml
Normal file
31
ansible/services/promtail/config/london-b.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
clients:
|
||||
- url: http://192.168.1.254:3100/loki/api/v1/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: london-b
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: varlogs
|
||||
instance: london-b
|
||||
__path__: /var/log/*log
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: plex
|
||||
instance: london-b
|
||||
__path__: /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Logs/*log
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: jellyfin
|
||||
instance: london-b
|
||||
__path__: /var/log/jellyfin/*log
|
||||
14
ansible/services/promtail/promtail.service
Normal file
14
ansible/services/promtail/promtail.service
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=Promtail service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/bin/promtail -config.file /etc/promtail/config.yml
|
||||
TimeoutSec=60
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
16
ansible/services/prowlarr/prowlarr.service
Normal file
16
ansible/services/prowlarr/prowlarr.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=Prowlarr Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0002
|
||||
Type=simple
|
||||
ExecStart=/opt/Prowlarr/Prowlarr -nobrowser -data=/var/lib/prowlarr/
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
16
ansible/services/radarr/radarr.service
Normal file
16
ansible/services/radarr/radarr.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=Radarr Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0002
|
||||
Type=simple
|
||||
ExecStart=/opt/Radarr/Radarr -nobrowser -data=/var/lib/radarr/
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
16
ansible/services/readarr/readarr.service
Normal file
16
ansible/services/readarr/readarr.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=Readarr Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0002
|
||||
Type=simple
|
||||
ExecStart=/opt/Readarr/Readarr -nobrowser -data=/var/lib/readarr/
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
53
ansible/services/samba/config/london-b.conf
Normal file
53
ansible/services/samba/config/london-b.conf
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[global]
|
||||
workgroup = WORKGROUP
|
||||
server string = %h server (Samba, Ubuntu)
|
||||
log file = /var/log/samba/log.%m
|
||||
max log size = 1000
|
||||
logging = file
|
||||
panic action = /usr/share/samba/panic-action %d
|
||||
server role = standalone server
|
||||
obey pam restrictions = yes
|
||||
unix password sync = yes
|
||||
passwd program = /usr/bin/passwd %u
|
||||
passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
|
||||
pam password change = yes
|
||||
map to guest = bad user
|
||||
|
||||
# COCKPIT ZFS MANAGER
|
||||
# WARNING: DO NOT EDIT, AUTO-GENERATED CONFIGURATION
|
||||
include = /etc/cockpit/zfs/shares.conf
|
||||
|
||||
[HDD]
|
||||
comment = HDD
|
||||
path = /hdd
|
||||
valid users = pez root
|
||||
public = no
|
||||
writable = yes
|
||||
|
||||
[Movies]
|
||||
comment = Movies
|
||||
path = /hdd/movies
|
||||
public = yes
|
||||
writable = no
|
||||
|
||||
[TV Shows]
|
||||
comment = TV Shows
|
||||
path = /hdd/tv
|
||||
public = yes
|
||||
writable = no
|
||||
|
||||
[printers]
|
||||
comment = All Printers
|
||||
browseable = no
|
||||
path = /var/tmp
|
||||
printable = yes
|
||||
guest ok = no
|
||||
read only = yes
|
||||
create mask = 0700
|
||||
|
||||
[print$]
|
||||
comment = Printer Drivers
|
||||
path = /var/lib/samba/printers
|
||||
browseable = yes
|
||||
read only = yes
|
||||
guest ok = no
|
||||
13
ansible/services/systemd/london-b/cloudflared.service
Normal file
13
ansible/services/systemd/london-b/cloudflared.service
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[Unit]
|
||||
Description=cloudflared
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
84
ansible/services/transmission/config/settings.json
Normal file
84
ansible/services/transmission/config/settings.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"alt-speed-down": 0,
|
||||
"alt-speed-enabled": false,
|
||||
"alt-speed-time-begin": 540,
|
||||
"alt-speed-time-day": 127,
|
||||
"alt-speed-time-enabled": false,
|
||||
"alt-speed-time-end": 1020,
|
||||
"alt-speed-up": 0,
|
||||
"announce-ip": "",
|
||||
"announce-ip-enabled": false,
|
||||
"anti-brute-force-enabled": false,
|
||||
"anti-brute-force-threshold": 100,
|
||||
"bind-address-ipv4": "0.0.0.0",
|
||||
"bind-address-ipv6": "::",
|
||||
"blocklist-enabled": false,
|
||||
"blocklist-url": "http://www.example.com/blocklist",
|
||||
"cache-size-mb": 4,
|
||||
"default-trackers": "",
|
||||
"dht-enabled": true,
|
||||
"download-dir": "/hdd/downloads",
|
||||
"download-limit": 100,
|
||||
"download-limit-enabled": 0,
|
||||
"download-queue-enabled": true,
|
||||
"download-queue-size": 100,
|
||||
"encryption": 1,
|
||||
"idle-seeding-limit": 30,
|
||||
"idle-seeding-limit-enabled": false,
|
||||
"incomplete-dir": "/var/lib/transmission-daemon/Downloads",
|
||||
"incomplete-dir-enabled": false,
|
||||
"lpd-enabled": false,
|
||||
"max-peers-global": 200,
|
||||
"message-level": 4,
|
||||
"peer-congestion-algorithm": "",
|
||||
"peer-id-ttl-hours": 6,
|
||||
"peer-limit-global": 200,
|
||||
"peer-limit-per-torrent": 50,
|
||||
"peer-port": 6881,
|
||||
"peer-port-random-high": 65535,
|
||||
"peer-port-random-low": 49152,
|
||||
"peer-port-random-on-start": false,
|
||||
"peer-socket-tos": "le",
|
||||
"pex-enabled": true,
|
||||
"port-forwarding-enabled": false,
|
||||
"preallocation": 1,
|
||||
"prefetch-enabled": true,
|
||||
"queue-stalled-enabled": true,
|
||||
"queue-stalled-minutes": 30,
|
||||
"ratio-limit": 0,
|
||||
"ratio-limit-enabled": true,
|
||||
"rename-partial-files": true,
|
||||
"rpc-authentication-required": false,
|
||||
"rpc-bind-address": "0.0.0.0",
|
||||
"rpc-enabled": true,
|
||||
"rpc-host-whitelist": "127.0.0.1,localhost,london-b,download.pez.sh,download.pez.solutions",
|
||||
"rpc-host-whitelist-enabled": false,
|
||||
"rpc-port": 9091,
|
||||
"rpc-socket-mode": "0750",
|
||||
"rpc-url": "/transmission/",
|
||||
"rpc-username": "transmission",
|
||||
"rpc-whitelist": "127.0.0.1,localhost,london-b,download.pez.sh,download.pez.solutions",
|
||||
"rpc-whitelist-enabled": false,
|
||||
"scrape-paused-torrents-enabled": true,
|
||||
"script-torrent-added-enabled": false,
|
||||
"script-torrent-added-filename": "",
|
||||
"script-torrent-done-enabled": false,
|
||||
"script-torrent-done-filename": "",
|
||||
"script-torrent-done-seeding-enabled": false,
|
||||
"script-torrent-done-seeding-filename": "",
|
||||
"seed-queue-enabled": false,
|
||||
"seed-queue-size": 10,
|
||||
"speed-limit-down": 50,
|
||||
"speed-limit-down-enabled": false,
|
||||
"speed-limit-up": 1000,
|
||||
"speed-limit-up-enabled": true,
|
||||
"start-added-torrents": true,
|
||||
"tcp-enabled": true,
|
||||
"torrent-added-verify-mode": "fast",
|
||||
"trash-original-torrent-files": false,
|
||||
"umask": "022",
|
||||
"upload-limit": 100,
|
||||
"upload-limit-enabled": 0,
|
||||
"upload-slots-per-torrent": 14,
|
||||
"utp-enabled": true
|
||||
}
|
||||
18
ansible/services/vsftpd/config/london-b.conf
Normal file
18
ansible/services/vsftpd/config/london-b.conf
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
listen=NO
|
||||
listen_ipv6=YES
|
||||
anonymous_enable=YES
|
||||
local_enable=YES
|
||||
write_enable=NO
|
||||
anon_upload_enable=NO
|
||||
dirmessage_enable=YES
|
||||
use_localtime=YES
|
||||
xferlog_enable=YES
|
||||
connect_from_port_20=YES
|
||||
ftpd_banner=Welcome to the Pez Dispenser
|
||||
secure_chroot_dir=/var/run/vsftpd/empty
|
||||
pam_service_name=vsftpd
|
||||
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
|
||||
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
|
||||
ssl_enable=NO
|
||||
anon_root=/hdd/ftp
|
||||
allow_writeable_chroot=YES
|
||||
16
ansible/services/whisparr/whisparr.service
Normal file
16
ansible/services/whisparr/whisparr.service
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=Whisparr Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0002
|
||||
Type=simple
|
||||
ExecStart=/opt/Whisparr/Whisparr -nobrowser -data=/var/lib/whisparr/
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -68,7 +68,49 @@ RAIDZ1 tolerates one drive failure per vdev. With this many drives and this much
|
|||
| smartctl_exporter | 9633 | (Prometheus scrape) |
|
||||
| prom-plex-exporter | — | (Prometheus scrape) |
|
||||
|
||||
All services run in Docker. Media is served directly from the ZFS pool.
|
||||
### Systemd Services (non-Docker)
|
||||
|
||||
The media automation suite and several supporting services run as native systemd units, not in Docker:
|
||||
|
||||
| Service | Unit Name | Notes |
|
||||
|---------|-----------|-------|
|
||||
| Sonarr | sonarr | Package-managed (mono) |
|
||||
| Radarr | radarr | /opt/Radarr, custom unit |
|
||||
| Prowlarr | prowlarr | /opt/Prowlarr, custom unit |
|
||||
| Lidarr | lidarr | /opt/Lidarr, custom unit |
|
||||
| Readarr | readarr | /opt/Readarr, custom unit |
|
||||
| Whisparr | whisparr | /opt/Whisparr, custom unit (disabled) |
|
||||
| Plex | plexmediaserver | Package-managed |
|
||||
| Jellyfin | jellyfin | Package-managed |
|
||||
| Transmission | transmission-daemon | Package-managed |
|
||||
| Samba | smbd | Package-managed |
|
||||
| Ollama | ollama | /usr/local/bin, custom unit |
|
||||
| Promtail | promtail | Custom unit, ships logs to Loki |
|
||||
| Cloudflared | cloudflared | Tunnel to Cloudflare |
|
||||
| vsftpd | vsftpd | FTP server for /hdd/ftp |
|
||||
| systemd_exporter | systemd_exporter | Ansible-managed |
|
||||
| node_exporter | node_exporter | Ansible-managed |
|
||||
|
||||
Docker services: Nextcloud AIO, Jellyseerr, Navidrome, slskd, Miniflux, smartctl-exporter, plex-exporter.
|
||||
|
||||
### Cron Jobs
|
||||
|
||||
| Schedule | Job |
|
||||
|----------|-----|
|
||||
| Every hour | `/root/scripts/movie-rename-fix.fish` |
|
||||
| Midnight daily | `systemctl restart radarr` |
|
||||
| Midnight daily | `systemctl restart sonarr` |
|
||||
| 22:00 daily | `/root/scripts/backup.sh` (rclone to B2) |
|
||||
|
||||
### Samba Shares
|
||||
|
||||
| Share | Path | Access |
|
||||
|-------|------|--------|
|
||||
| HDD | /hdd | pez, root (rw) |
|
||||
| Movies | /hdd/movies | public (ro) |
|
||||
| TV Shows | /hdd/tv | public (ro) |
|
||||
|
||||
Media is served directly from the ZFS pool.
|
||||
|
||||
## Networking
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue