mirror of
https://github.com/rancher/os.git
synced 2025-09-03 15:54:24 +00:00
refactor a little and keep the datasource errors for later
Signed-off-by: Sven Dowideit <SvenDowideit@home.org.au>
This commit is contained in:
@@ -20,12 +20,10 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/rancher/os/cmd/control"
|
||||
"github.com/rancher/os/cmd/network"
|
||||
rancherConfig "github.com/rancher/os/config"
|
||||
@@ -49,9 +47,6 @@ const (
|
||||
datasourceInterval = 100 * time.Millisecond
|
||||
datasourceMaxInterval = 30 * time.Second
|
||||
datasourceTimeout = 5 * time.Minute
|
||||
configDevName = "config-2"
|
||||
configDev = "LABEL=" + configDevName
|
||||
configDevMountPoint = "/media/config-2"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
@@ -70,29 +65,8 @@ func Main() {
|
||||
}
|
||||
}
|
||||
|
||||
func MountConfigDrive() error {
|
||||
if err := os.MkdirAll(configDevMountPoint, 644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configDev := util.ResolveDevice(configDev)
|
||||
|
||||
if configDev == "" {
|
||||
return mount.Mount(configDevName, configDevMountPoint, "9p", "trans=virtio,version=9p2000.L")
|
||||
}
|
||||
|
||||
fsType, err := util.GetFsType(configDev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.Mount(configDev, configDevMountPoint, fsType, "ro")
|
||||
}
|
||||
|
||||
func UnmountConfigDrive() error {
|
||||
return syscall.Unmount(configDevMountPoint, 0)
|
||||
}
|
||||
|
||||
func SaveCloudConfig(network bool) error {
|
||||
log.Debugf("SaveCloudConfig")
|
||||
userDataBytes, metadata, err := fetchUserData(network)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -178,6 +152,7 @@ func currentDatasource(network bool) (datasource.Datasource, error) {
|
||||
|
||||
dss := getDatasources(cfg, network)
|
||||
if len(dss) == 0 {
|
||||
log.Errorf("currentDatasource - none found")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -299,11 +274,13 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
|
||||
duration := datasourceInterval
|
||||
for {
|
||||
log.Infof("Checking availability of %q\n", s.Type())
|
||||
log.Infof("cloud-init: Checking availability of %q\n", s.Type())
|
||||
if s.IsAvailable() {
|
||||
ds <- s
|
||||
return
|
||||
} else if !s.AvailabilityChanges() {
|
||||
}
|
||||
log.Errorf("cloud-init: Datasource not ready: %s", s)
|
||||
if !s.AvailabilityChanges() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
|
72
config/cloudinit/datasource/configdrive/configdrive.go
Normal file → Executable file
72
config/cloudinit/datasource/configdrive/configdrive.go
Normal file → Executable file
@@ -16,30 +16,59 @@ package configdrive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
const (
|
||||
configDevName = "config-2"
|
||||
configDev = "LABEL=" + configDevName
|
||||
configDevMountPoint = "/media/config-2"
|
||||
openstackAPIVersion = "latest"
|
||||
)
|
||||
|
||||
type ConfigDrive struct {
|
||||
root string
|
||||
readFile func(filename string) ([]byte, error)
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *ConfigDrive {
|
||||
return &ConfigDrive{root, ioutil.ReadFile}
|
||||
return &ConfigDrive{root, ioutil.ReadFile, nil}
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) IsAvailable() bool {
|
||||
_, err := os.Stat(cd.root)
|
||||
return !os.IsNotExist(err)
|
||||
if cd.root == configDevMountPoint {
|
||||
cd.lastError = MountConfigDrive()
|
||||
if cd.lastError != nil {
|
||||
log.Error(cd.lastError)
|
||||
return false
|
||||
}
|
||||
defer cd.Finish()
|
||||
}
|
||||
|
||||
_, cd.lastError = os.Stat(cd.root)
|
||||
return !os.IsNotExist(cd.lastError)
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) Finish() error {
|
||||
return UnmountConfigDrive()
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) String() string {
|
||||
if cd.lastError != nil {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", cd.Type(), cd.root, cd.lastError)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", cd.Type(), cd.root)
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) AvailabilityChanges() bool {
|
||||
@@ -93,10 +122,45 @@ func (cd *ConfigDrive) openstackVersionRoot() string {
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) tryReadFile(filename string) ([]byte, error) {
|
||||
if cd.root == configDevMountPoint {
|
||||
cd.lastError = MountConfigDrive()
|
||||
if cd.lastError != nil {
|
||||
log.Error(cd.lastError)
|
||||
return nil, cd.lastError
|
||||
}
|
||||
defer cd.Finish()
|
||||
}
|
||||
log.Printf("Attempting to read from %q\n", filename)
|
||||
data, err := cd.readFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("ERROR read cloud-config file(%s) - err: %q", filename, err)
|
||||
} else {
|
||||
log.Debugf("SUCCESS read cloud-config file(%s) - date: %s", filename, string(data))
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func MountConfigDrive() error {
|
||||
if err := os.MkdirAll(configDevMountPoint, 644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configDev := util.ResolveDevice(configDev)
|
||||
|
||||
if configDev == "" {
|
||||
return mount.Mount(configDevName, configDevMountPoint, "9p", "trans=virtio,version=9p2000.L")
|
||||
}
|
||||
|
||||
fsType, err := util.GetFsType(configDev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.Mount(configDev, configDevMountPoint, fsType, "ro")
|
||||
}
|
||||
|
||||
func UnmountConfigDrive() error {
|
||||
return syscall.Unmount(configDevMountPoint, 0)
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ func TestFetchMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
cd := ConfigDrive{tt.root, tt.files.ReadFile}
|
||||
cd := ConfigDrive{tt.root, tt.files.ReadFile, nil}
|
||||
metadata, err := cd.FetchMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %+v: want %v, got %q", tt, nil, err)
|
||||
@@ -91,7 +91,7 @@ func TestFetchUserdata(t *testing.T) {
|
||||
"userdata",
|
||||
},
|
||||
} {
|
||||
cd := ConfigDrive{tt.root, tt.files.ReadFile}
|
||||
cd := ConfigDrive{tt.root, tt.files.ReadFile, nil}
|
||||
userdata, err := cd.FetchUserdata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %+v: want %v, got %q", tt, nil, err)
|
||||
@@ -116,7 +116,7 @@ func TestConfigRoot(t *testing.T) {
|
||||
"/media/configdrive/openstack",
|
||||
},
|
||||
} {
|
||||
cd := ConfigDrive{tt.root, nil}
|
||||
cd := ConfigDrive{tt.root, nil, nil}
|
||||
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
|
||||
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
|
||||
}
|
||||
|
3
config/cloudinit/datasource/datasource.go
Normal file → Executable file
3
config/cloudinit/datasource/datasource.go
Normal file → Executable file
@@ -25,6 +25,9 @@ type Datasource interface {
|
||||
FetchMetadata() (Metadata, error)
|
||||
FetchUserdata() ([]byte, error)
|
||||
Type() string
|
||||
String() string
|
||||
// Finish gives the datasource the oportunity to clean up, unmount or release any open / cache resources
|
||||
Finish() error
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
|
16
config/cloudinit/datasource/file/file.go
Normal file → Executable file
16
config/cloudinit/datasource/file/file.go
Normal file → Executable file
@@ -15,6 +15,7 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
@@ -23,15 +24,24 @@ import (
|
||||
|
||||
type LocalFile struct {
|
||||
path string
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource(path string) *LocalFile {
|
||||
return &LocalFile{path}
|
||||
return &LocalFile{path, nil}
|
||||
}
|
||||
|
||||
func (f *LocalFile) IsAvailable() bool {
|
||||
_, err := os.Stat(f.path)
|
||||
return !os.IsNotExist(err)
|
||||
_, f.lastError = os.Stat(f.path)
|
||||
return !os.IsNotExist(f.lastError)
|
||||
}
|
||||
|
||||
func (f *LocalFile) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *LocalFile) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", f.Type(), f.path, f.lastError)
|
||||
}
|
||||
|
||||
func (f *LocalFile) AvailabilityChanges() bool {
|
||||
|
16
config/cloudinit/datasource/metadata/metadata.go
Normal file → Executable file
16
config/cloudinit/datasource/metadata/metadata.go
Normal file → Executable file
@@ -15,6 +15,7 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -27,18 +28,27 @@ type Service struct {
|
||||
APIVersion string
|
||||
UserdataPath string
|
||||
MetadataPath string
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource(root, apiVersion, userdataPath, metadataPath string, header http.Header) Service {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
return Service{root, pkg.NewHTTPClientHeader(header), apiVersion, userdataPath, metadataPath}
|
||||
return Service{root, pkg.NewHTTPClientHeader(header), apiVersion, userdataPath, metadataPath, nil}
|
||||
}
|
||||
|
||||
func (ms Service) IsAvailable() bool {
|
||||
_, err := ms.Client.Get(ms.Root + ms.APIVersion)
|
||||
return (err == nil)
|
||||
_, ms.lastError = ms.Client.Get(ms.Root + ms.APIVersion)
|
||||
return (ms.lastError == nil)
|
||||
}
|
||||
|
||||
func (ms *Service) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *Service) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", "metadata", ms.Root+":"+ms.UserdataPath, ms.lastError)
|
||||
}
|
||||
|
||||
func (ms Service) AvailabilityChanges() bool {
|
||||
|
22
config/cloudinit/datasource/proccmdline/proc_cmdline.go
Normal file → Executable file
22
config/cloudinit/datasource/proccmdline/proc_cmdline.go
Normal file → Executable file
@@ -16,10 +16,12 @@ package proccmdline
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
"github.com/rancher/os/config/cloudinit/pkg"
|
||||
)
|
||||
@@ -31,6 +33,7 @@ const (
|
||||
|
||||
type ProcCmdline struct {
|
||||
Location string
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource() *ProcCmdline {
|
||||
@@ -38,14 +41,23 @@ func NewDatasource() *ProcCmdline {
|
||||
}
|
||||
|
||||
func (c *ProcCmdline) IsAvailable() bool {
|
||||
contents, err := ioutil.ReadFile(c.Location)
|
||||
if err != nil {
|
||||
var contents []byte
|
||||
contents, c.lastError = ioutil.ReadFile(c.Location)
|
||||
if c.lastError != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
cmdline := strings.TrimSpace(string(contents))
|
||||
_, err = findCloudConfigURL(cmdline)
|
||||
return (err == nil)
|
||||
_, c.lastError = findCloudConfigURL(cmdline)
|
||||
return (c.lastError == nil)
|
||||
}
|
||||
|
||||
func (c *ProcCmdline) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ProcCmdline) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", c.Type(), c.Location, c.lastError)
|
||||
}
|
||||
|
||||
func (c *ProcCmdline) AvailabilityChanges() bool {
|
||||
|
0
config/cloudinit/datasource/test/filesystem.go
Normal file → Executable file
0
config/cloudinit/datasource/test/filesystem.go
Normal file → Executable file
17
config/cloudinit/datasource/url/url.go
Normal file → Executable file
17
config/cloudinit/datasource/url/url.go
Normal file → Executable file
@@ -15,22 +15,33 @@
|
||||
package url
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
"github.com/rancher/os/config/cloudinit/pkg"
|
||||
)
|
||||
|
||||
type RemoteFile struct {
|
||||
url string
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource(url string) *RemoteFile {
|
||||
return &RemoteFile{url}
|
||||
return &RemoteFile{url, nil}
|
||||
}
|
||||
|
||||
func (f *RemoteFile) IsAvailable() bool {
|
||||
client := pkg.NewHTTPClient()
|
||||
_, err := client.Get(f.url)
|
||||
return (err == nil)
|
||||
_, f.lastError = client.Get(f.url)
|
||||
return (f.lastError == nil)
|
||||
}
|
||||
|
||||
func (f *RemoteFile) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *RemoteFile) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", f.Type(), f.url, f.lastError)
|
||||
}
|
||||
|
||||
func (f *RemoteFile) AvailabilityChanges() bool {
|
||||
|
9
config/cloudinit/datasource/vmware/vmware.go
Normal file → Executable file
9
config/cloudinit/datasource/vmware/vmware.go
Normal file → Executable file
@@ -29,6 +29,15 @@ type VMWare struct {
|
||||
ovfFileName string
|
||||
readConfig readConfigFunction
|
||||
urlDownload urlDownloadFunction
|
||||
lastError error
|
||||
}
|
||||
|
||||
func (v VMWare) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v VMWare) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", v.Type(), v.ovfFileName, v.lastError)
|
||||
}
|
||||
|
||||
func (v VMWare) AvailabilityChanges() bool {
|
||||
|
9
config/cloudinit/datasource/vmware/vmware_amd64.go
Normal file → Executable file
9
config/cloudinit/datasource/vmware/vmware_amd64.go
Normal file → Executable file
@@ -16,14 +16,15 @@ package vmware
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/pkg"
|
||||
|
||||
"github.com/sigma/vmw-guestinfo/rpcvmx"
|
||||
"github.com/sigma/vmw-guestinfo/vmcheck"
|
||||
"github.com/sigma/vmw-ovflib"
|
||||
ovf "github.com/sigma/vmw-ovflib"
|
||||
)
|
||||
|
||||
type ovfWrapper struct {
|
||||
@@ -69,8 +70,8 @@ func NewDatasource(fileName string) *VMWare {
|
||||
|
||||
func (v VMWare) IsAvailable() bool {
|
||||
if v.ovfFileName != "" {
|
||||
_, err := os.Stat(v.ovfFileName)
|
||||
return !os.IsNotExist(err)
|
||||
_, v.lastError = os.Stat(v.ovfFileName)
|
||||
return !os.IsNotExist(v.lastError)
|
||||
}
|
||||
return vmcheck.IsVirtualWorld()
|
||||
}
|
||||
|
19
config/cloudinit/datasource/waagent/waagent.go
Normal file → Executable file
19
config/cloudinit/datasource/waagent/waagent.go
Normal file → Executable file
@@ -16,27 +16,38 @@ package waagent
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
)
|
||||
|
||||
type Waagent struct {
|
||||
root string
|
||||
readFile func(filename string) ([]byte, error)
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *Waagent {
|
||||
return &Waagent{root, ioutil.ReadFile}
|
||||
return &Waagent{root, ioutil.ReadFile, nil}
|
||||
}
|
||||
|
||||
func (a *Waagent) IsAvailable() bool {
|
||||
_, err := os.Stat(path.Join(a.root, "provisioned"))
|
||||
return !os.IsNotExist(err)
|
||||
_, a.lastError = os.Stat(path.Join(a.root, "provisioned"))
|
||||
return !os.IsNotExist(a.lastError)
|
||||
}
|
||||
|
||||
func (a *Waagent) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Waagent) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", a.Type(), a.root, a.lastError)
|
||||
}
|
||||
|
||||
func (a *Waagent) AvailabilityChanges() bool {
|
||||
|
@@ -86,7 +86,7 @@ func TestFetchMetadata(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
a := Waagent{tt.root, tt.files.ReadFile}
|
||||
a := Waagent{tt.root, tt.files.ReadFile, nil}
|
||||
metadata, err := a.FetchMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %+v: want %v, got %q", tt, nil, err)
|
||||
@@ -115,7 +115,7 @@ func TestFetchUserdata(t *testing.T) {
|
||||
test.NewMockFilesystem(test.File{Path: "/var/lib/Waagent/CustomData", Contents: ""}),
|
||||
},
|
||||
} {
|
||||
a := Waagent{tt.root, tt.files.ReadFile}
|
||||
a := Waagent{tt.root, tt.files.ReadFile, nil}
|
||||
_, err := a.FetchUserdata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %+v: want %v, got %q", tt, nil, err)
|
||||
@@ -137,7 +137,7 @@ func TestConfigRoot(t *testing.T) {
|
||||
"/var/lib/Waagent",
|
||||
},
|
||||
} {
|
||||
a := Waagent{tt.root, nil}
|
||||
a := Waagent{tt.root, nil, nil}
|
||||
if configRoot := a.ConfigRoot(); configRoot != tt.configRoot {
|
||||
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
|
||||
}
|
||||
|
22
init/init.go
Normal file → Executable file
22
init/init.go
Normal file → Executable file
@@ -12,7 +12,6 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/rancher/os/cmd/cloudinitsave"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/dfs"
|
||||
"github.com/rancher/os/log"
|
||||
@@ -294,29 +293,10 @@ func RunInit() error {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
network := false
|
||||
for _, datasource := range cfg.Rancher.CloudInit.Datasources {
|
||||
if cloudinitsave.RequiresNetwork(datasource) {
|
||||
network = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if network {
|
||||
if err := runCloudInitServices(cfg); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
if err := cloudinitsave.MountConfigDrive(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := cloudinitsave.SaveCloudConfig(false); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := cloudinitsave.UnmountConfigDrive(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
},
|
||||
func(cfg *config.CloudConfig) (*config.CloudConfig, error) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
rancher:
|
||||
debug: true
|
||||
environment:
|
||||
VERSION: {{.VERSION}}
|
||||
SUFFIX: {{.SUFFIX}}
|
||||
@@ -66,6 +67,7 @@ rancher:
|
||||
cloud_init:
|
||||
datasources:
|
||||
- configdrive:/media/config-2
|
||||
- url:http://example.com/404-error
|
||||
repositories:
|
||||
core:
|
||||
url: {{.OS_SERVICES_REPO}}/{{.REPO_VERSION}}
|
||||
|
@@ -48,4 +48,4 @@ REBUILD=1
|
||||
QEMUARCH=${qemuarch["${ARCH}"]}
|
||||
TTYCONS=${ttycons["${ARCH}"]}
|
||||
|
||||
DEFAULT_KERNEL_ARGS="rancher.debug=false rancher.password=rancher console=${TTYCONS} rancher.autologin=${TTYCONS}"
|
||||
DEFAULT_KERNEL_ARGS="printk.devkmsg=on rancher.debug=true rancher.password=rancher console=${TTYCONS} rancher.autologin=${TTYCONS}"
|
||||
|
Reference in New Issue
Block a user