mirror of
https://github.com/rancher/os.git
synced 2025-08-04 16:30:15 +00:00
Split cloud-init into cloud-init-execute and cloud-init-save
This commit is contained in:
parent
e5f1f299f0
commit
889cb9eea8
@ -1,4 +1,4 @@
|
|||||||
package cloudinit
|
package cloudinitexecute
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
131
cmd/cloudinitexecute/cloudinitexecute.go
Normal file
131
cmd/cloudinitexecute/cloudinitexecute.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package cloudinitexecute
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/mount"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
|
"github.com/rancher/os/config"
|
||||||
|
"github.com/rancher/os/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
resizeStamp = "/var/lib/rancher/resizefs.done"
|
||||||
|
sshKeyName = "rancheros-cloud-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
console bool
|
||||||
|
preConsole bool
|
||||||
|
flags *flag.FlagSet
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||||
|
flags.BoolVar(&console, "console", false, "apply console configuration")
|
||||||
|
flags.BoolVar(&preConsole, "pre-console", false, "apply pre-console configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
flags.Parse(os.Args[1:])
|
||||||
|
|
||||||
|
log.Infof("Running cloud-init-execute: pre-console=%v, console=%v", preConsole, console)
|
||||||
|
|
||||||
|
cfg := config.LoadConfig()
|
||||||
|
|
||||||
|
if !console && !preConsole {
|
||||||
|
console = true
|
||||||
|
preConsole = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if console {
|
||||||
|
applyConsole(cfg)
|
||||||
|
}
|
||||||
|
if preConsole {
|
||||||
|
applyPreConsole(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyConsole(cfg *config.CloudConfig) {
|
||||||
|
if len(cfg.SSHAuthorizedKeys) > 0 {
|
||||||
|
authorizeSSHKeys("rancher", cfg.SSHAuthorizedKeys, sshKeyName)
|
||||||
|
authorizeSSHKeys("docker", cfg.SSHAuthorizedKeys, sshKeyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range cfg.WriteFiles {
|
||||||
|
f := system.File{File: file}
|
||||||
|
fullPath, err := system.WriteFile(&f, "/")
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{"err": err, "path": fullPath}).Error("Error writing file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("Wrote file %s to filesystem", fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, configMount := range cfg.Mounts {
|
||||||
|
if len(configMount) != 4 {
|
||||||
|
log.Errorf("Unable to mount %s: must specify exactly four arguments", configMount[1])
|
||||||
|
}
|
||||||
|
device := util.ResolveDevice(configMount[0])
|
||||||
|
if configMount[2] == "swap" {
|
||||||
|
cmd := exec.Command("swapon", device)
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to swapon %s: %v", device, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := mount.Mount(device, configMount[1], configMount[2], configMount[3]); err != nil {
|
||||||
|
log.Errorf("Unable to mount %s: %v", configMount[1], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyPreConsole(cfg *config.CloudConfig) {
|
||||||
|
if _, err := os.Stat(resizeStamp); os.IsNotExist(err) && cfg.Rancher.ResizeDevice != "" {
|
||||||
|
if err := resizeDevice(cfg); err == nil {
|
||||||
|
os.Create(resizeStamp)
|
||||||
|
} else {
|
||||||
|
log.Errorf("Failed to resize %s: %s", cfg.Rancher.ResizeDevice, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range cfg.Rancher.Sysctl {
|
||||||
|
elems := []string{"/proc", "sys"}
|
||||||
|
elems = append(elems, strings.Split(k, ".")...)
|
||||||
|
path := path.Join(elems...)
|
||||||
|
if err := ioutil.WriteFile(path, []byte(v), 0644); err != nil {
|
||||||
|
log.Errorf("Failed to set sysctl key %s: %s", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizeDevice(cfg *config.CloudConfig) error {
|
||||||
|
cmd := exec.Command("growpart", cfg.Rancher.ResizeDevice, "1")
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("partprobe")
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("resize2fs", fmt.Sprintf("%s1", cfg.Rancher.ResizeDevice))
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -13,22 +13,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package cloudinit
|
package cloudinitsave
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||||
"github.com/docker/docker/pkg/mount"
|
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/coreos/coreos-cloudinit/config"
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
@ -41,9 +36,8 @@ import (
|
|||||||
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
|
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
|
||||||
"github.com/coreos/coreos-cloudinit/datasource/url"
|
"github.com/coreos/coreos-cloudinit/datasource/url"
|
||||||
"github.com/coreos/coreos-cloudinit/pkg"
|
"github.com/coreos/coreos-cloudinit/pkg"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
"github.com/rancher/netconf"
|
"github.com/rancher/netconf"
|
||||||
"github.com/rancher/os/cmd/cloudinit/gce"
|
"github.com/rancher/os/cmd/cloudinitsave/gce"
|
||||||
rancherConfig "github.com/rancher/os/config"
|
rancherConfig "github.com/rancher/os/config"
|
||||||
"github.com/rancher/os/util"
|
"github.com/rancher/os/util"
|
||||||
)
|
)
|
||||||
@ -52,13 +46,9 @@ const (
|
|||||||
datasourceInterval = 100 * time.Millisecond
|
datasourceInterval = 100 * time.Millisecond
|
||||||
datasourceMaxInterval = 30 * time.Second
|
datasourceMaxInterval = 30 * time.Second
|
||||||
datasourceTimeout = 5 * time.Minute
|
datasourceTimeout = 5 * time.Minute
|
||||||
sshKeyName = "rancheros-cloud-config"
|
|
||||||
resizeStamp = "/var/lib/rancher/resizefs.done"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
save bool
|
|
||||||
execute bool
|
|
||||||
network bool
|
network bool
|
||||||
flags *flag.FlagSet
|
flags *flag.FlagSet
|
||||||
)
|
)
|
||||||
@ -66,8 +56,16 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||||
flags.BoolVar(&network, "network", true, "use network based datasources")
|
flags.BoolVar(&network, "network", true, "use network based datasources")
|
||||||
flags.BoolVar(&save, "save", false, "save cloud config and exit")
|
}
|
||||||
flags.BoolVar(&execute, "execute", false, "execute saved cloud config")
|
|
||||||
|
func Main() {
|
||||||
|
flags.Parse(os.Args[1:])
|
||||||
|
|
||||||
|
log.Infof("Running cloud-init-save: network=%v", network)
|
||||||
|
|
||||||
|
if err := saveCloudConfig(); err != nil {
|
||||||
|
log.Errorf("Failed to save cloud-config: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFiles(cloudConfigBytes, scriptBytes []byte, metadata datasource.Metadata) error {
|
func saveFiles(cloudConfigBytes, scriptBytes []byte, metadata datasource.Metadata) error {
|
||||||
@ -171,104 +169,6 @@ func fetchUserData() ([]byte, datasource.Metadata, error) {
|
|||||||
return userDataBytes, metadata, nil
|
return userDataBytes, metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resizeDevice(cfg *rancherConfig.CloudConfig) error {
|
|
||||||
cmd := exec.Command("growpart", cfg.Rancher.ResizeDevice, "1")
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = exec.Command("partprobe")
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = exec.Command("resize2fs", fmt.Sprintf("%s1", cfg.Rancher.ResizeDevice))
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeCloudConfig() error {
|
|
||||||
cc := rancherConfig.LoadConfig()
|
|
||||||
|
|
||||||
if len(cc.SSHAuthorizedKeys) > 0 {
|
|
||||||
authorizeSSHKeys("rancher", cc.SSHAuthorizedKeys, sshKeyName)
|
|
||||||
authorizeSSHKeys("docker", cc.SSHAuthorizedKeys, sshKeyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range cc.WriteFiles {
|
|
||||||
f := system.File{File: file}
|
|
||||||
fullPath, err := system.WriteFile(&f, "/")
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"err": err, "path": fullPath}).Error("Error writing file")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Printf("Wrote file %s to filesystem", fullPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(resizeStamp); os.IsNotExist(err) && cc.Rancher.ResizeDevice != "" {
|
|
||||||
if err := resizeDevice(cc); err == nil {
|
|
||||||
os.Create(resizeStamp)
|
|
||||||
} else {
|
|
||||||
log.Errorf("Failed to resize %s: %s", cc.Rancher.ResizeDevice, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, configMount := range cc.Mounts {
|
|
||||||
if len(configMount) != 4 {
|
|
||||||
log.Errorf("Unable to mount %s: must specify exactly four arguments", configMount[1])
|
|
||||||
}
|
|
||||||
device := util.ResolveDevice(configMount[0])
|
|
||||||
if configMount[2] == "swap" {
|
|
||||||
cmd := exec.Command("swapon", device)
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to swapon %s: %v", device, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := mount.Mount(device, configMount[1], configMount[2], configMount[3]); err != nil {
|
|
||||||
log.Errorf("Unable to mount %s: %v", configMount[1], err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range cc.Rancher.Sysctl {
|
|
||||||
elems := []string{"/proc", "sys"}
|
|
||||||
elems = append(elems, strings.Split(k, ".")...)
|
|
||||||
path := path.Join(elems...)
|
|
||||||
if err := ioutil.WriteFile(path, []byte(v), 0644); err != nil {
|
|
||||||
log.Errorf("Failed to set sysctl key %s: %s", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Main() {
|
|
||||||
flags.Parse(os.Args[1:])
|
|
||||||
|
|
||||||
log.Infof("Running cloud-init: save=%v, execute=%v", save, execute)
|
|
||||||
|
|
||||||
if save {
|
|
||||||
err := saveCloudConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"err": err}).Error("Failed to save cloud-config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if execute {
|
|
||||||
err := executeCloudConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"err": err}).Error("Failed to execute cloud-config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDatasources creates a slice of possible Datasources for cloudinit based
|
// getDatasources creates a slice of possible Datasources for cloudinit based
|
||||||
// on the different source command-line flags.
|
// on the different source command-line flags.
|
||||||
func getDatasources(cfg *rancherConfig.CloudConfig) []datasource.Datasource {
|
func getDatasources(cfg *rancherConfig.CloudConfig) []datasource.Datasource {
|
@ -1,4 +1,4 @@
|
|||||||
package cloudinit
|
package cloudinitsave
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -12,4 +12,4 @@ else
|
|||||||
mount -t 9p -o trans=virtio,version=9p2000.L config-2 ${MOUNT_POINT} 2>/dev/null || true
|
mount -t 9p -o trans=virtio,version=9p2000.L config-2 ${MOUNT_POINT} 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cloud-init -save -network=${CLOUD_INIT_NETWORK:-true}
|
cloud-init-save -network=${CLOUD_INIT_NETWORK:-true}
|
||||||
|
@ -115,7 +115,7 @@ EOF
|
|||||||
echo 'RancherOS \n \l' > /etc/issue
|
echo 'RancherOS \n \l' > /etc/issue
|
||||||
echo $(/sbin/ifconfig | grep -B1 "inet addr" |awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' |awk -F: '{ print $1 ": " $3}') >> /etc/issue
|
echo $(/sbin/ifconfig | grep -B1 "inet addr" |awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' |awk -F: '{ print $1 ": " $3}') >> /etc/issue
|
||||||
|
|
||||||
cloud-init -execute
|
cloud-init-execute -console
|
||||||
|
|
||||||
if [ -x /var/lib/rancher/conf/cloud-config-script ]; then
|
if [ -x /var/lib/rancher/conf/cloud-config-script ]; then
|
||||||
echo "Running /var/lib/rancher/conf/cloud-config-script"
|
echo "Running /var/lib/rancher/conf/cloud-config-script"
|
||||||
|
40
main.go
40
main.go
@ -7,7 +7,8 @@ import (
|
|||||||
"github.com/docker/docker/pkg/reexec"
|
"github.com/docker/docker/pkg/reexec"
|
||||||
"github.com/rancher/cniglue"
|
"github.com/rancher/cniglue"
|
||||||
"github.com/rancher/docker-from-scratch"
|
"github.com/rancher/docker-from-scratch"
|
||||||
"github.com/rancher/os/cmd/cloudinit"
|
"github.com/rancher/os/cmd/cloudinitexecute"
|
||||||
|
"github.com/rancher/os/cmd/cloudinitsave"
|
||||||
"github.com/rancher/os/cmd/control"
|
"github.com/rancher/os/cmd/control"
|
||||||
"github.com/rancher/os/cmd/network"
|
"github.com/rancher/os/cmd/network"
|
||||||
"github.com/rancher/os/cmd/power"
|
"github.com/rancher/os/cmd/power"
|
||||||
@ -21,24 +22,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var entrypoints = map[string]func(){
|
var entrypoints = map[string]func(){
|
||||||
"cloud-init": cloudinit.Main,
|
"cloud-init-execute": cloudinitexecute.Main,
|
||||||
"docker": docker.Main,
|
"cloud-init-save": cloudinitsave.Main,
|
||||||
"dockerlaunch": dockerlaunch.Main,
|
"docker": docker.Main,
|
||||||
"halt": power.Halt,
|
"dockerlaunch": dockerlaunch.Main,
|
||||||
"init": osInit.MainInit,
|
"halt": power.Halt,
|
||||||
"netconf": network.Main,
|
"init": osInit.MainInit,
|
||||||
"poweroff": power.PowerOff,
|
"netconf": network.Main,
|
||||||
"reboot": power.Reboot,
|
"poweroff": power.PowerOff,
|
||||||
"respawn": respawn.Main,
|
"reboot": power.Reboot,
|
||||||
"ros-sysinit": sysinit.Main,
|
"respawn": respawn.Main,
|
||||||
"shutdown": power.Main,
|
"ros-sysinit": sysinit.Main,
|
||||||
"switch-console": switchconsole.Main,
|
"shutdown": power.Main,
|
||||||
"system-docker": systemdocker.Main,
|
"switch-console": switchconsole.Main,
|
||||||
"user-docker": userdocker.Main,
|
"system-docker": systemdocker.Main,
|
||||||
"wait-for-docker": wait.Main,
|
"user-docker": userdocker.Main,
|
||||||
"cni-glue": glue.Main,
|
"wait-for-docker": wait.Main,
|
||||||
"bridge": bridge.Main,
|
"cni-glue": glue.Main,
|
||||||
"host-local": hostlocal.Main,
|
"bridge": bridge.Main,
|
||||||
|
"host-local": hostlocal.Main,
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -120,6 +120,19 @@ rancher:
|
|||||||
volumes_from:
|
volumes_from:
|
||||||
- command-volumes
|
- command-volumes
|
||||||
- system-volumes
|
- system-volumes
|
||||||
|
cloud-init-execute:
|
||||||
|
image: {{.OS_REPO}}/os-base:{{.VERSION}}{{.SUFFIX}}
|
||||||
|
command: cloud-init-execute -pre-console
|
||||||
|
labels:
|
||||||
|
io.rancher.os.detach: "false"
|
||||||
|
io.rancher.os.scope: system
|
||||||
|
io.rancher.os.after: cloud-init
|
||||||
|
net: host
|
||||||
|
uts: host
|
||||||
|
privileged: true
|
||||||
|
volumes_from:
|
||||||
|
- command-volumes
|
||||||
|
- system-volumes
|
||||||
cloud-init-pre:
|
cloud-init-pre:
|
||||||
image: {{.OS_REPO}}/os-cloudinit:{{.VERSION}}{{.SUFFIX}}
|
image: {{.OS_REPO}}/os-cloudinit:{{.VERSION}}{{.SUFFIX}}
|
||||||
environment:
|
environment:
|
||||||
@ -159,7 +172,8 @@ rancher:
|
|||||||
- /usr/bin/ros:/sbin/shutdown:ro
|
- /usr/bin/ros:/sbin/shutdown:ro
|
||||||
- /usr/bin/ros:/usr/bin/respawn:ro
|
- /usr/bin/ros:/usr/bin/respawn:ro
|
||||||
- /usr/bin/ros:/usr/bin/ros:ro
|
- /usr/bin/ros:/usr/bin/ros:ro
|
||||||
- /usr/bin/ros:/usr/bin/cloud-init:ro
|
- /usr/bin/ros:/usr/bin/cloud-init-execute:ro
|
||||||
|
- /usr/bin/ros:/usr/bin/cloud-init-save:ro
|
||||||
- /usr/bin/ros:/usr/sbin/netconf:ro
|
- /usr/bin/ros:/usr/sbin/netconf:ro
|
||||||
- /usr/bin/ros:/usr/sbin/wait-for-docker:ro
|
- /usr/bin/ros:/usr/sbin/wait-for-docker:ro
|
||||||
- /usr/bin/ros:/usr/bin/switch-console:ro
|
- /usr/bin/ros:/usr/bin/switch-console:ro
|
||||||
@ -212,7 +226,7 @@ rancher:
|
|||||||
command: netconf --stop-network-pre
|
command: netconf --stop-network-pre
|
||||||
labels:
|
labels:
|
||||||
io.rancher.os.scope: system
|
io.rancher.os.scope: system
|
||||||
io.rancher.os.after: cloud-init
|
io.rancher.os.after: cloud-init-execute
|
||||||
net: host
|
net: host
|
||||||
uts: host
|
uts: host
|
||||||
pid: host
|
pid: host
|
||||||
|
@ -7,6 +7,6 @@ func (s *QemuSuite) TestSwap(c *C) {
|
|||||||
c.Assert(err, IsNil)
|
c.Assert(err, IsNil)
|
||||||
|
|
||||||
s.CheckCall(c, "sudo mkswap /dev/vdb")
|
s.CheckCall(c, "sudo mkswap /dev/vdb")
|
||||||
s.CheckCall(c, "sudo cloud-init -execute")
|
s.CheckCall(c, "sudo cloud-init-execute")
|
||||||
s.CheckCall(c, "cat /proc/swaps | grep /dev/vdb")
|
s.CheckCall(c, "cat /proc/swaps | grep /dev/vdb")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user