change zfs list handling
This commit is contained in:
parent
2704149a16
commit
e192906265
12
app.go
12
app.go
@ -402,7 +402,7 @@ func (a AppConfig) ExecBefore(schedule string) error {
|
|||||||
for k, v := range a.Before {
|
for k, v := range a.Before {
|
||||||
re := regexp.MustCompile(k)
|
re := regexp.MustCompile(k)
|
||||||
if re.MatchString(schedule) {
|
if re.MatchString(schedule) {
|
||||||
err := cfg.Box[v.Box()].SSHExec(v.Path())
|
_, err := cfg.Box[v.Box()].SSHExec(v.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.ExecBefore : %s : Error executing %s", a.Name, string(v))
|
log.Printf("AppConfig.ExecBefore : %s : Error executing %s", a.Name, string(v))
|
||||||
@ -422,7 +422,7 @@ func (a AppConfig) ExecAfter(schedule string) error {
|
|||||||
for k, v := range a.After {
|
for k, v := range a.After {
|
||||||
re := regexp.MustCompile(k)
|
re := regexp.MustCompile(k)
|
||||||
if re.MatchString(schedule) {
|
if re.MatchString(schedule) {
|
||||||
err := cfg.Box[v.Box()].SSHExec(v.Path())
|
_, err := cfg.Box[v.Box()].SSHExec(v.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.ExecAfter : %s : Error executing %s on %s", a.Name, v.Path(), v.Box())
|
log.Printf("AppConfig.ExecAfter : %s : Error executing %s on %s", a.Name, v.Path(), v.Box())
|
||||||
@ -505,7 +505,7 @@ func (a AppConfig) SendSnapshots() error {
|
|||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.SendSnapshots : %s : Initializing snapshot on %s from %s", a.Name, dest.Box(), string(sFirstSnapshot))
|
log.Printf("AppConfig.SendSnapshots : %s : Initializing snapshot on %s from %s", a.Name, dest.Box(), string(sFirstSnapshot))
|
||||||
}
|
}
|
||||||
err = cfg.Box[dest.Box()].SSHExec("ssh " + cfg.Box[src.Box()].User + "@" + src.Box() + " zfs send " + string(sFirstSnapshot) + " | zfs recv -F " + dest.Path() + "/" + src.Box() + "/" + src.Path())
|
_, err = cfg.Box[dest.Box()].SSHExec("ssh " + cfg.Box[src.Box()].User + "@" + src.Box() + " zfs send " + string(sFirstSnapshot) + " | zfs recv -F " + dest.Path() + "/" + src.Box() + "/" + src.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.SendSnapshots : %s : Initializing snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sFirstSnapshot), err)
|
log.Printf("AppConfig.SendSnapshots : %s : Initializing snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sFirstSnapshot), err)
|
||||||
@ -520,7 +520,7 @@ func (a AppConfig) SendSnapshots() error {
|
|||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.SendSnapshots : %s : Sending incrementally %s to %s", a.Name, string(sNextSnapshot), dest.Box())
|
log.Printf("AppConfig.SendSnapshots : %s : Sending incrementally %s to %s", a.Name, string(sNextSnapshot), dest.Box())
|
||||||
}
|
}
|
||||||
err = cfg.Box[dest.Box()].SSHExec("ssh " + cfg.Box[src.Box()].User + "@" + src.Box() + " zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path())
|
_, err = cfg.Box[dest.Box()].SSHExec("ssh " + cfg.Box[src.Box()].User + "@" + src.Box() + " zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.SendSnapshots : %s : Sending snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sNextSnapshot), err)
|
log.Printf("AppConfig.SendSnapshots : %s : Sending snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sNextSnapshot), err)
|
||||||
@ -544,7 +544,7 @@ func (a AppConfig) SendSnapshots() error {
|
|||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.SendSnapshots : %s : Sending incrementally %s to %s", a.Name, string(sNextSnapshot), dest.Box())
|
log.Printf("AppConfig.SendSnapshots : %s : Sending incrementally %s to %s", a.Name, string(sNextSnapshot), dest.Box())
|
||||||
}
|
}
|
||||||
err = cfg.Box[dest.Box()].SSHExec("ssh " + cfg.Box[src.Box()].User + "@" + src.Box() + " zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path())
|
_, err = cfg.Box[dest.Box()].SSHExec("ssh " + cfg.Box[src.Box()].User + "@" + src.Box() + " zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.SendSnapshots : %s : Sending snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sNextSnapshot), err)
|
log.Printf("AppConfig.SendSnapshots : %s : Sending snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sNextSnapshot), err)
|
||||||
@ -576,7 +576,7 @@ func (a AppConfig) CleanupSnapshot() error {
|
|||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.CleanupSnapshot : %s : cleaning snapshots on %s for%s", a.Name, k, v)
|
log.Printf("AppConfig.CleanupSnapshot : %s : cleaning snapshots on %s for%s", a.Name, k, v)
|
||||||
}
|
}
|
||||||
err := cfg.Box[k].SSHExec("zfsnap destroy -p hourly- -p daily- -p weekly- -p monthly- -p yearly-" + v)
|
_, err := cfg.Box[k].SSHExec("zfsnap destroy -p hourly- -p daily- -p weekly- -p monthly- -p yearly-" + v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("AppConfig.CleanupSnapshot : %s : Error executing zfsnap on %s", a.Name, k)
|
log.Printf("AppConfig.CleanupSnapshot : %s : Error executing zfsnap on %s", a.Name, k)
|
||||||
|
112
box.go
112
box.go
@ -1,8 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
@ -11,6 +14,7 @@ type Box struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Name string `json:"-"`
|
Name string `json:"-"`
|
||||||
ssh *SSHConfig
|
ssh *SSHConfig
|
||||||
|
zfs *ZFSConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Box) ZFSGetLastSnapshot(path string) (s Snapshot, err error) {
|
func (b *Box) ZFSGetLastSnapshot(path string) (s Snapshot, err error) {
|
||||||
@ -38,19 +42,114 @@ func (b *Box) ZFSGetSnapshotList() []Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Box) ZFSUpdateList() (err error) {
|
func (b *Box) ZFSUpdateList() (err error) {
|
||||||
return b.ssh.getZFSList()
|
b.zfs.M.Lock()
|
||||||
|
if b.zfs.ZFSDeleted || b.zfs.ZFSAdded {
|
||||||
|
b.zfs.ZFSInitialized = false
|
||||||
|
}
|
||||||
|
b.zfs.M.Unlock()
|
||||||
|
|
||||||
|
err = b.ZFSInitialize()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Box) ZFSIsZFS(path string) bool {
|
func (b *Box) ZFSIsZFS(path string) bool {
|
||||||
return b.ssh.isZFS(path)
|
err := b.ZFSInitialize()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
b.zfs.M.Lock()
|
||||||
|
defer b.zfs.M.Unlock()
|
||||||
|
|
||||||
|
if _, ok := b.zfs.ZFSMap[path]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Box) ZFSCreateZFS(path string) (err error) {
|
func (b *Box) ZFSCreateZFS(path string) (err error) {
|
||||||
return b.ssh.createZFS(path)
|
err = b.ZFSInitialize()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Box) SSHExec(cmd string) (err error) {
|
b.zfs.M.Lock()
|
||||||
return b.ssh.exec(cmd)
|
defer b.zfs.M.Unlock()
|
||||||
|
|
||||||
|
p := strings.Split(path, `/`)
|
||||||
|
var base string
|
||||||
|
for _, d := range p {
|
||||||
|
if base == "" {
|
||||||
|
base = d
|
||||||
|
} else {
|
||||||
|
base = base + `/` + d
|
||||||
|
}
|
||||||
|
if _, ok := b.zfs.ZFSMap[base]; !ok {
|
||||||
|
|
||||||
|
if *debugFlag {
|
||||||
|
log.Printf("Box.ZFSCreateZFS : Creating %s:%s", b.Name, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.SSHExec("zfs create -o mountpoint=none " + base)
|
||||||
|
if err != nil {
|
||||||
|
if *debugFlag {
|
||||||
|
log.Printf("Box.ZFSCreateZFS : %s : SSHExec : %s", b.Name, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.zfs.ZFSMap[base] = "none"
|
||||||
|
b.zfs.ZFSAdded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Box) ZFSInitialize() (err error) {
|
||||||
|
b.zfs.M.Lock()
|
||||||
|
defer b.zfs.M.Unlock()
|
||||||
|
|
||||||
|
if b.zfs.ZFSInitialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if *debugFlag {
|
||||||
|
log.Printf("Box.ZFSInitialize : %s : Start", b.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.zfs.ZFSMap = make(map[string]string)
|
||||||
|
|
||||||
|
var buf *bytes.Buffer
|
||||||
|
buf, err = b.SSHExec("zfs list -H -o name,mountpoint")
|
||||||
|
|
||||||
|
csvReader := csv.NewReader(buf)
|
||||||
|
csvReader.Comma = '\t'
|
||||||
|
csvReader.FieldsPerRecord = 2
|
||||||
|
|
||||||
|
csvData, err := csvReader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
if *debugFlag {
|
||||||
|
log.Printf("Box.ZFSInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range csvData {
|
||||||
|
b.zfs.ZFSMap[rec[0]] = rec[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
b.zfs.ZFSInitialized = true
|
||||||
|
b.zfs.ZFSAdded = false
|
||||||
|
b.zfs.ZFSDeleted = false
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Box) SSHExec(cmd string) (buf *bytes.Buffer, err error) {
|
||||||
|
buf, err = b.ssh.exec(cmd)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
|
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
|
||||||
@ -59,5 +158,6 @@ func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
|
|||||||
}
|
}
|
||||||
timestamp := cfg.Now.Format("2006-01-02_15.04.05")
|
timestamp := cfg.Now.Format("2006-01-02_15.04.05")
|
||||||
name := fmt.Sprintf("%s-%s--%s", schedule, timestamp, cfg.Zfsnap[schedule])
|
name := fmt.Sprintf("%s-%s--%s", schedule, timestamp, cfg.Zfsnap[schedule])
|
||||||
return b.ssh.exec("zfs snapshot " + path + "@" + name)
|
_, err = b.ssh.exec("zfs snapshot " + path + "@" + name)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ func (c *Config) Load() error {
|
|||||||
|
|
||||||
for k, v := range c.Box {
|
for k, v := range c.Box {
|
||||||
v.Name = k
|
v.Name = k
|
||||||
|
v.zfs = NewZFSConfig()
|
||||||
s := &SSHConfig{
|
s := &SSHConfig{
|
||||||
logged: false,
|
logged: false,
|
||||||
name: k,
|
name: k,
|
||||||
|
131
ssh.go
131
ssh.go
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@ -145,82 +144,7 @@ func (s *SSHConfig) getSnapshotList() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSHConfig) getZFSList() error {
|
func (s *SSHConfig) exec(cmd string) (b *bytes.Buffer, err error) {
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.getZFSList : %s : Start", s.name)
|
|
||||||
}
|
|
||||||
if !s.logged {
|
|
||||||
return fmt.Errorf("Client %s not logged in.", s.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := s.client.NewSession()
|
|
||||||
if err != nil {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.getZFSList : %s : client.NewSession() : %s", s.name, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
session.Stdout = &b
|
|
||||||
|
|
||||||
err = session.Run("TZ=\"" + cfg.Timezone + "\" zfs list -H -o name,mountpoint")
|
|
||||||
if err != nil {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.getZFSList : %s : session.Run() : %s", s.name, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.zfs = make(map[string]string)
|
|
||||||
|
|
||||||
csvReader := csv.NewReader(&b)
|
|
||||||
csvReader.Comma = '\t'
|
|
||||||
csvReader.FieldsPerRecord = 2
|
|
||||||
|
|
||||||
csvData, err := csvReader.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.getZFSList : %s : csvReader.ReadAll() : %s", s.name, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rec := range csvData {
|
|
||||||
s.zfs[rec[0]] = rec[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.getZFSList : %s : read %d zfs file systems", s.name, len(s.zfs))
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHConfig) isZFS(path string) bool {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.isZFS : Start %s:%s", s.name, path)
|
|
||||||
}
|
|
||||||
if len(s.zfs) == 0 {
|
|
||||||
err := s.getZFSList()
|
|
||||||
if err != nil {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.isZFS : s.getZFSList(%s) : %s", s.name, err)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, ok := s.zfs[path]
|
|
||||||
if ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHConfig) exec(cmd string) error {
|
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("SSHConfig.exec : %s : Start %s", s.name, cmd)
|
log.Printf("SSHConfig.exec : %s : Start %s", s.name, cmd)
|
||||||
}
|
}
|
||||||
@ -230,61 +154,22 @@ func (s *SSHConfig) exec(cmd string) error {
|
|||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("SSHConfig.exec : %s : client().NewSession(%s) : %s", s.name, cmd, err)
|
log.Printf("SSHConfig.exec : %s : client().NewSession(%s) : %s", s.name, cmd, err)
|
||||||
}
|
}
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
b = &buf
|
||||||
|
session.Stdout = b
|
||||||
|
|
||||||
err = session.Run("TZ=\"" + cfg.Timezone + "\" " + cmd)
|
err = session.Run("TZ=\"" + cfg.Timezone + "\" " + cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if *debugFlag {
|
if *debugFlag {
|
||||||
log.Printf("SSHConfig.exec : session(%s).Run(%s) : %s", s.name, cmd, err)
|
log.Printf("SSHConfig.exec : session(%s).Run(%s) : %s", s.name, cmd, err)
|
||||||
}
|
}
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session.Close()
|
session.Close()
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SSHConfig) createZFS(path string) error {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.createZFS : Start %s:%s", s.name, path)
|
|
||||||
}
|
|
||||||
if len(s.zfs) == 0 {
|
|
||||||
err := s.getZFSList()
|
|
||||||
if err != nil {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.createZFS : s.getZFSList(%s) : %s", s.name, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p := strings.Split(path, `/`)
|
|
||||||
var base string
|
|
||||||
for _, d := range p {
|
|
||||||
if base == "" {
|
|
||||||
base = d
|
|
||||||
} else {
|
|
||||||
base = base + `/` + d
|
|
||||||
}
|
|
||||||
if _, ok := s.zfs[base]; !ok {
|
|
||||||
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.createZFS : Creating %s:%s", s.name, base)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.exec("zfs create -o mountpoint=none " + base)
|
|
||||||
if err != nil {
|
|
||||||
if *debugFlag {
|
|
||||||
log.Printf("SSHConfig.createZFS : s.exec(%s) : %s", s.name, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.zfs[base] = "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Code generated by version.sh (@generated) DO NOT EDIT.
|
// Code generated by version.sh (@generated) DO NOT EDIT.
|
||||||
package main
|
package main
|
||||||
var githash = "119d069"
|
var githash = "2704149"
|
||||||
var buildstamp = "2021-11-14_04:20:23"
|
var buildstamp = "2021-11-14_07:57:44"
|
||||||
var commits = "18"
|
var commits = "20"
|
||||||
var version = "119d069-b18 - 2021-11-14_04:20:23"
|
var version = "2704149-b20 - 2021-11-14_07:57:44"
|
||||||
|
26
zfs.go
26
zfs.go
@ -1 +1,27 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type ZFSConfig struct {
|
||||||
|
SnapshotAdded bool
|
||||||
|
SnapshotDeleted bool
|
||||||
|
SnapshotInitialized bool
|
||||||
|
SnapshotList []Snapshot
|
||||||
|
ZFSAdded bool
|
||||||
|
ZFSDeleted bool
|
||||||
|
ZFSInitialized bool
|
||||||
|
ZFSMap map[string]string
|
||||||
|
M sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZFSConfig() (z *ZFSConfig) {
|
||||||
|
z = &ZFSConfig{
|
||||||
|
SnapshotAdded: false,
|
||||||
|
SnapshotDeleted: false,
|
||||||
|
SnapshotInitialized: false,
|
||||||
|
ZFSAdded: false,
|
||||||
|
ZFSDeleted: false,
|
||||||
|
ZFSInitialized: false,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user