forked from github/multus-cni
commit
7094c9675f
@ -10,6 +10,8 @@ MULTUS_CONF_FILE="/usr/src/multus-cni/images/70-multus.conf"
|
|||||||
MULTUS_BIN_FILE="/usr/src/multus-cni/bin/multus"
|
MULTUS_BIN_FILE="/usr/src/multus-cni/bin/multus"
|
||||||
MULTUS_KUBECONFIG_FILE_HOST="/etc/cni/net.d/multus.d/multus.kubeconfig"
|
MULTUS_KUBECONFIG_FILE_HOST="/etc/cni/net.d/multus.d/multus.kubeconfig"
|
||||||
MULTUS_NAMESPACE_ISOLATION=false
|
MULTUS_NAMESPACE_ISOLATION=false
|
||||||
|
MULTUS_LOG_LEVEL=""
|
||||||
|
MULTUS_LOG_FILE=""
|
||||||
|
|
||||||
# Give help text for parameters.
|
# Give help text for parameters.
|
||||||
function usage()
|
function usage()
|
||||||
@ -17,7 +19,7 @@ function usage()
|
|||||||
echo -e "This is an entrypoint script for Multus CNI to overlay its binary and "
|
echo -e "This is an entrypoint script for Multus CNI to overlay its binary and "
|
||||||
echo -e "configuration into locations in a filesystem. The configuration & binary file "
|
echo -e "configuration into locations in a filesystem. The configuration & binary file "
|
||||||
echo -e "will be copied to the corresponding configuration directory. When "
|
echo -e "will be copied to the corresponding configuration directory. When "
|
||||||
echo -e "`--multus-conf-file=auto` is used, 00-multus.conf will be automatically "
|
echo -e "'--multus-conf-file=auto' is used, 00-multus.conf will be automatically "
|
||||||
echo -e "generated from the CNI configuration file of the master plugin (the first file "
|
echo -e "generated from the CNI configuration file of the master plugin (the first file "
|
||||||
echo -e "in lexicographical order in cni-conf-dir)."
|
echo -e "in lexicographical order in cni-conf-dir)."
|
||||||
echo -e ""
|
echo -e ""
|
||||||
@ -29,6 +31,8 @@ function usage()
|
|||||||
echo -e "\t--multus-bin-file=$MULTUS_BIN_FILE"
|
echo -e "\t--multus-bin-file=$MULTUS_BIN_FILE"
|
||||||
echo -e "\t--multus-kubeconfig-file-host=$MULTUS_KUBECONFIG_FILE_HOST"
|
echo -e "\t--multus-kubeconfig-file-host=$MULTUS_KUBECONFIG_FILE_HOST"
|
||||||
echo -e "\t--namespace-isolation=$MULTUS_NAMESPACE_ISOLATION"
|
echo -e "\t--namespace-isolation=$MULTUS_NAMESPACE_ISOLATION"
|
||||||
|
echo -e "\t--multus-log-level=$MULTUS_LOG_LEVEL (empty by default, used only with --multus-conf-file=auto)"
|
||||||
|
echo -e "\t--multus-log-file=$MULTUS_LOG_FILE (empty by default, used only with --multus-conf-file=auto)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse parameters given as arguments to this script.
|
# Parse parameters given as arguments to this script.
|
||||||
@ -58,10 +62,14 @@ while [ "$1" != "" ]; do
|
|||||||
--namespace-isolation)
|
--namespace-isolation)
|
||||||
MULTUS_NAMESPACE_ISOLATION=$VALUE
|
MULTUS_NAMESPACE_ISOLATION=$VALUE
|
||||||
;;
|
;;
|
||||||
|
--multus-log-level)
|
||||||
|
MULTUS_LOG_LEVEL=$VALUE
|
||||||
|
;;
|
||||||
|
--multus-log-file)
|
||||||
|
MULTUS_LOG_FILE=$VALUE
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "ERROR: unknown parameter \"$PARAM\""
|
echo "WARNING: unknown parameter \"$PARAM\""
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@ -71,7 +79,7 @@ done
|
|||||||
# Create array of known locations
|
# Create array of known locations
|
||||||
declare -a arr=($CNI_CONF_DIR $CNI_BIN_DIR $MULTUS_BIN_FILE)
|
declare -a arr=($CNI_CONF_DIR $CNI_BIN_DIR $MULTUS_BIN_FILE)
|
||||||
if [ "$MULTUS_CONF_FILE" != "auto" ]; then
|
if [ "$MULTUS_CONF_FILE" != "auto" ]; then
|
||||||
arr+=($MULTUS_BIN_FILE)
|
arr+=($MULTUS_CONF_FILE)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@ -157,32 +165,73 @@ fi
|
|||||||
|
|
||||||
if [ "$MULTUS_CONF_FILE" == "auto" ]; then
|
if [ "$MULTUS_CONF_FILE" == "auto" ]; then
|
||||||
echo "Generating Multus configuration file ..."
|
echo "Generating Multus configuration file ..."
|
||||||
MASTER_PLUGIN="$(ls $CNI_CONF_DIR | grep -E '\.conf(list)?$' | head -1)"
|
found_master=false
|
||||||
if [ "$MASTER_PLUGIN" == "" ]; then
|
tries=0
|
||||||
echo "Error: Multus could not be configured: no master plugin was found."
|
while [ $found_master == false ]; do
|
||||||
exit 1;
|
MASTER_PLUGIN="$(ls $CNI_CONF_DIR | grep -E '\.conf(list)?$' | grep -Ev '00-multus\.conf' | head -1)"
|
||||||
elif [ "$MASTER_PLUGIN" == "00-multus.conf" ]; then
|
if [ "$MASTER_PLUGIN" == "" ]; then
|
||||||
echo "Warning: Multus is already configured: auto configuration skipped."
|
if [ $tries -lt 600 ]; then
|
||||||
else
|
if ! (($tries % 5)); then
|
||||||
ISOLATION_STRING=""
|
echo "Attemping to find master plugin configuration, attempt $tries"
|
||||||
if [ "$MULTUS_NAMESPACE_ISOLATION" == true ]; then
|
fi
|
||||||
ISOLATION_STRING="\"namespaceIsolation\": true,"
|
let "tries+=1"
|
||||||
|
sleep 1;
|
||||||
|
else
|
||||||
|
echo "Error: Multus could not be configured: no master plugin was found."
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
|
||||||
|
found_master=true
|
||||||
|
|
||||||
|
ISOLATION_STRING=""
|
||||||
|
if [ "$MULTUS_NAMESPACE_ISOLATION" == true ]; then
|
||||||
|
ISOLATION_STRING="\"namespaceIsolation\": true,"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOG_LEVEL_STRING=""
|
||||||
|
if [ ! -z "${MULTUS_LOG_LEVEL// }" ]; then
|
||||||
|
case "$MULTUS_LOG_LEVEL" in
|
||||||
|
debug)
|
||||||
|
;;
|
||||||
|
error)
|
||||||
|
;;
|
||||||
|
panic)
|
||||||
|
;;
|
||||||
|
verbose)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Log levels should be one of: debug/verbose/error/panic, did not understand $MULTUS_LOG_LEVEL"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
|
LOG_LEVEL_STRING="\"logLevel\": \"$MULTUS_LOG_LEVEL\","
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOG_FILE_STRING=""
|
||||||
|
if [ ! -z "${MULTUS_LOG_FILE// }" ]; then
|
||||||
|
LOG_FILE_STRING="\"logFile\": \"$MULTUS_LOG_FILE\","
|
||||||
|
fi
|
||||||
|
|
||||||
|
MASTER_PLUGIN_JSON="$(cat $CNI_CONF_DIR/$MASTER_PLUGIN)"
|
||||||
|
CONF=$(cat <<-EOF
|
||||||
|
{
|
||||||
|
"name": "multus-cni-network",
|
||||||
|
"type": "multus",
|
||||||
|
$ISOLATION_STRING
|
||||||
|
$LOG_LEVEL_STRING
|
||||||
|
$LOG_FILE_STRING
|
||||||
|
"kubeconfig": "$MULTUS_KUBECONFIG_FILE_HOST",
|
||||||
|
"delegates": [
|
||||||
|
$MASTER_PLUGIN_JSON
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
echo $CONF > $CNI_CONF_DIR/00-multus.conf
|
||||||
|
echo "Config file created @ $CNI_CONF_DIR/00-multus.conf"
|
||||||
fi
|
fi
|
||||||
MASTER_PLUGIN_JSON="$(cat $CNI_CONF_DIR/$MASTER_PLUGIN)"
|
done
|
||||||
CONF=$(cat <<-EOF
|
|
||||||
{
|
|
||||||
"name": "multus-cni-network",
|
|
||||||
"type": "multus",
|
|
||||||
$ISOLATION_STRING
|
|
||||||
"kubeconfig": "$MULTUS_KUBECONFIG_FILE_HOST",
|
|
||||||
"delegates": [
|
|
||||||
$MASTER_PLUGIN_JSON
|
|
||||||
]
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
echo $CONF > $CNI_CONF_DIR/00-multus.conf
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------- end Generate "00-multus.conf".
|
# ---------------------- end Generate "00-multus.conf".
|
||||||
|
@ -544,7 +544,7 @@ func GetPodNetwork(k8sclient KubeClient, k8sArgs *types.K8sArgs, confdir string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultNetDelegateCRD(client KubeClient, net, confdir, namespace string) (*types.DelegateNetConf, error) {
|
func getDefaultNetDelegateCRD(client KubeClient, net, confdir, namespace string) (*types.DelegateNetConf, error) {
|
||||||
logging.Debugf("getDefaultNetDelegate: %v, %v, %s", client, net, confdir)
|
logging.Debugf("getDefaultNetDelegateCRD: %v, %v, %s, %s", client, net, confdir, namespace)
|
||||||
rawPath := fmt.Sprintf("/apis/k8s.cni.cncf.io/v1/namespaces/%s/network-attachment-definitions/%s", namespace, net)
|
rawPath := fmt.Sprintf("/apis/k8s.cni.cncf.io/v1/namespaces/%s/network-attachment-definitions/%s", namespace, net)
|
||||||
netData, err := client.GetRawWithPath(rawPath)
|
netData, err := client.GetRawWithPath(rawPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -570,7 +570,7 @@ func getDefaultNetDelegateCRD(client KubeClient, net, confdir, namespace string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getNetDelegate(client KubeClient, netname, confdir, namespace string) (*types.DelegateNetConf, error) {
|
func getNetDelegate(client KubeClient, netname, confdir, namespace string) (*types.DelegateNetConf, error) {
|
||||||
logging.Debugf("getNetDelegate: %v, %v, %v", client, netname, confdir)
|
logging.Debugf("getNetDelegate: %v, %v, %v, %s", client, netname, confdir, namespace)
|
||||||
// option1) search CRD object for the network
|
// option1) search CRD object for the network
|
||||||
delegate, err := getDefaultNetDelegateCRD(client, netname, confdir, namespace)
|
delegate, err := getDefaultNetDelegateCRD(client, netname, confdir, namespace)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -681,8 +681,8 @@ func tryLoadK8sPodDefaultNetwork(k8sArgs *types.K8sArgs, conf *types.NetConf, ku
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CRD object of default network should only be defined in default namespace
|
// The CRD object of default network should only be defined in multusNamespace
|
||||||
networks, err := parsePodNetworkAnnotation(netAnnot, "default")
|
networks, err := parsePodNetworkAnnotation(netAnnot, conf.MultusNamespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, logging.Errorf("tryLoadK8sPodDefaultNetwork: failed to parse CRD object: %v", err)
|
return nil, logging.Errorf("tryLoadK8sPodDefaultNetwork: failed to parse CRD object: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -480,6 +480,7 @@ var _ = Describe("k8sclient operations", func() {
|
|||||||
"name":"node-cni-network",
|
"name":"node-cni-network",
|
||||||
"type":"multus",
|
"type":"multus",
|
||||||
"clusterNetwork": "net2",
|
"clusterNetwork": "net2",
|
||||||
|
"multusNamespace" : "kube-system",
|
||||||
"kubeconfig":"/etc/kubernetes/node-kubeconfig.yaml"
|
"kubeconfig":"/etc/kubernetes/node-kubeconfig.yaml"
|
||||||
}`
|
}`
|
||||||
netConf, err := types.LoadNetConf([]byte(conf))
|
netConf, err := types.LoadNetConf([]byte(conf))
|
||||||
@ -490,7 +491,7 @@ var _ = Describe("k8sclient operations", func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fKubeClient := testutils.NewFakeKubeClient()
|
fKubeClient := testutils.NewFakeKubeClient()
|
||||||
fKubeClient.AddNetConfig("default", "net1", "{\"type\": \"mynet1\"}")
|
fKubeClient.AddNetConfig("kube-system", "net1", "{\"type\": \"mynet1\"}")
|
||||||
fKubeClient.AddNetConfig("kube-system", "net2", "{\"type\": \"mynet2\"}")
|
fKubeClient.AddNetConfig("kube-system", "net2", "{\"type\": \"mynet2\"}")
|
||||||
fKubeClient.AddPod(fakePod)
|
fKubeClient.AddPod(fakePod)
|
||||||
kubeClient, err := GetK8sClient("", fKubeClient)
|
kubeClient, err := GetK8sClient("", fKubeClient)
|
||||||
@ -534,7 +535,7 @@ var _ = Describe("k8sclient operations", func() {
|
|||||||
|
|
||||||
fKubeClient := testutils.NewFakeKubeClient()
|
fKubeClient := testutils.NewFakeKubeClient()
|
||||||
fKubeClient.AddPod(fakePod)
|
fKubeClient.AddPod(fakePod)
|
||||||
fKubeClient.AddNetConfig("default", "net1", "{\"type\": \"mynet1\"}")
|
fKubeClient.AddNetConfig("kube-system", "net1", "{\"type\": \"mynet1\"}")
|
||||||
kubeClient, err := GetK8sClient("", fKubeClient)
|
kubeClient, err := GetK8sClient("", fKubeClient)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
k8sArgs, err := GetK8sArgs(args)
|
k8sArgs, err := GetK8sArgs(args)
|
||||||
|
@ -29,6 +29,7 @@ type Level uint32
|
|||||||
const (
|
const (
|
||||||
PanicLevel Level = iota
|
PanicLevel Level = iota
|
||||||
ErrorLevel
|
ErrorLevel
|
||||||
|
VerboseLevel
|
||||||
DebugLevel
|
DebugLevel
|
||||||
MaxLevel
|
MaxLevel
|
||||||
UnknownLevel
|
UnknownLevel
|
||||||
@ -44,6 +45,8 @@ func (l Level) String() string {
|
|||||||
switch l {
|
switch l {
|
||||||
case PanicLevel:
|
case PanicLevel:
|
||||||
return "panic"
|
return "panic"
|
||||||
|
case VerboseLevel:
|
||||||
|
return "verbose"
|
||||||
case ErrorLevel:
|
case ErrorLevel:
|
||||||
return "error"
|
return "error"
|
||||||
case DebugLevel:
|
case DebugLevel:
|
||||||
@ -76,6 +79,10 @@ func Debugf(format string, a ...interface{}) {
|
|||||||
Printf(DebugLevel, format, a...)
|
Printf(DebugLevel, format, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Verbosef(format string, a ...interface{}) {
|
||||||
|
Printf(VerboseLevel, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
func Errorf(format string, a ...interface{}) error {
|
func Errorf(format string, a ...interface{}) error {
|
||||||
Printf(ErrorLevel, format, a...)
|
Printf(ErrorLevel, format, a...)
|
||||||
return fmt.Errorf(format, a...)
|
return fmt.Errorf(format, a...)
|
||||||
@ -88,10 +95,16 @@ func Panicf(format string, a ...interface{}) {
|
|||||||
Printf(PanicLevel, "========= Stack trace output end ========")
|
Printf(PanicLevel, "========= Stack trace output end ========")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLoggingLevel(levelStr string) Level {
|
func GetLoggingLevel() Level {
|
||||||
|
return loggingLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoggingLevel(levelStr string) Level {
|
||||||
switch strings.ToLower(levelStr) {
|
switch strings.ToLower(levelStr) {
|
||||||
case "debug":
|
case "debug":
|
||||||
return DebugLevel
|
return DebugLevel
|
||||||
|
case "verbose":
|
||||||
|
return VerboseLevel
|
||||||
case "error":
|
case "error":
|
||||||
return ErrorLevel
|
return ErrorLevel
|
||||||
case "panic":
|
case "panic":
|
||||||
@ -102,7 +115,7 @@ func GetLoggingLevel(levelStr string) Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetLogLevel(levelStr string) {
|
func SetLogLevel(levelStr string) {
|
||||||
level := GetLoggingLevel(levelStr)
|
level := getLoggingLevel(levelStr)
|
||||||
if level < MaxLevel {
|
if level < MaxLevel {
|
||||||
loggingLevel = level
|
loggingLevel = level
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,8 @@ var _ = Describe("logging operations", func() {
|
|||||||
Expect(loggingLevel).To(Equal(DebugLevel))
|
Expect(loggingLevel).To(Equal(DebugLevel))
|
||||||
SetLogLevel("Error")
|
SetLogLevel("Error")
|
||||||
Expect(loggingLevel).To(Equal(ErrorLevel))
|
Expect(loggingLevel).To(Equal(ErrorLevel))
|
||||||
|
SetLogLevel("VERbose")
|
||||||
|
Expect(loggingLevel).To(Equal(VerboseLevel))
|
||||||
SetLogLevel("PANIC")
|
SetLogLevel("PANIC")
|
||||||
Expect(loggingLevel).To(Equal(PanicLevel))
|
Expect(loggingLevel).To(Equal(PanicLevel))
|
||||||
})
|
})
|
||||||
|
@ -208,18 +208,30 @@ func delegateAdd(exec invoke.Exec, ifName string, delegate *types.DelegateNetCon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if delegate.ConfListPlugin != false {
|
var result cnitypes.Result
|
||||||
result, err := conflistAdd(rt, delegate.Bytes, binDir, exec)
|
var err error
|
||||||
|
if delegate.ConfListPlugin {
|
||||||
|
result, err = conflistAdd(rt, delegate.Bytes, binDir, exec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, logging.Errorf("Multus: error in invoke Conflist add - %q: %v", delegate.ConfList.Name, err)
|
return nil, logging.Errorf("Multus: error in invoke Conflist add - %q: %v", delegate.ConfList.Name, err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return result, nil
|
result, err = invoke.DelegateAdd(delegate.Conf.Type, delegate.Bytes, exec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, logging.Errorf("Multus: error in invoke Delegate add - %q: %v", delegate.Conf.Type, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := invoke.DelegateAdd(delegate.Conf.Type, delegate.Bytes, exec)
|
if logging.GetLoggingLevel() >= logging.VerboseLevel {
|
||||||
if err != nil {
|
data, _ := json.Marshal(result)
|
||||||
return nil, logging.Errorf("Multus: error in invoke Delegate add - %q: %v", delegate.Conf.Type, err)
|
var confName string
|
||||||
|
if delegate.ConfListPlugin {
|
||||||
|
confName = delegate.ConfList.Name
|
||||||
|
} else {
|
||||||
|
confName = delegate.Conf.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Verbosef("Add: %s:%s:%s:%s %s", rt.Args[1][1], rt.Args[2][1], confName, rt.IfName, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@ -231,20 +243,29 @@ func delegateDel(exec invoke.Exec, ifName string, delegateConf *types.DelegateNe
|
|||||||
return logging.Errorf("Multus: error in setting CNI_IFNAME")
|
return logging.Errorf("Multus: error in setting CNI_IFNAME")
|
||||||
}
|
}
|
||||||
|
|
||||||
if delegateConf.ConfListPlugin != false {
|
if logging.GetLoggingLevel() >= logging.VerboseLevel {
|
||||||
err := conflistDel(rt, delegateConf.Bytes, binDir, exec)
|
var confName string
|
||||||
|
if delegateConf.ConfListPlugin {
|
||||||
|
confName = delegateConf.ConfList.Name
|
||||||
|
} else {
|
||||||
|
confName = delegateConf.Conf.Name
|
||||||
|
}
|
||||||
|
logging.Verbosef("Del: %s:%s:%s:%s %s", rt.Args[1][1], rt.Args[2][1], confName, rt.IfName, string(delegateConf.Bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if delegateConf.ConfListPlugin {
|
||||||
|
err = conflistDel(rt, delegateConf.Bytes, binDir, exec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return logging.Errorf("Multus: error in invoke Conflist Del - %q: %v", delegateConf.ConfList.Name, err)
|
return logging.Errorf("Multus: error in invoke Conflist Del - %q: %v", delegateConf.ConfList.Name, err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return err
|
if err = invoke.DelegateDel(delegateConf.Conf.Type, delegateConf.Bytes, exec); err != nil {
|
||||||
|
return logging.Errorf("Multus: error in invoke Delegate del - %q: %v", delegateConf.Conf.Type, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := invoke.DelegateDel(delegateConf.Conf.Type, delegateConf.Bytes, exec); err != nil {
|
return err
|
||||||
return logging.Errorf("Multus: error in invoke Delegate del - %q: %v", delegateConf.Conf.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func delPlugins(exec invoke.Exec, argIfname string, delegates []*types.DelegateNetConf, lastIdx int, rt *libcni.RuntimeConf, binDir string) error {
|
func delPlugins(exec invoke.Exec, argIfname string, delegates []*types.DelegateNetConf, lastIdx int, rt *libcni.RuntimeConf, binDir string) error {
|
||||||
|
Loading…
Reference in New Issue
Block a user