mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
CLOUDSTACK-6179 Execute VR commands on Virtual Resource when commands received to Hyper-V
This commit is contained in:
parent
33a0dec965
commit
540d020aa5
@ -53,7 +53,7 @@ public class HypervGuru extends HypervisorGuruBase implements HypervisorGuru {
|
|||||||
@Inject HypervManager _hypervMgr;
|
@Inject HypervManager _hypervMgr;
|
||||||
@Inject NetworkDao _networkDao;
|
@Inject NetworkDao _networkDao;
|
||||||
@Inject NetworkModel _networkMgr;
|
@Inject NetworkModel _networkMgr;
|
||||||
|
int MaxNicSupported = 8;
|
||||||
@Override
|
@Override
|
||||||
public final HypervisorType getHypervisorType() {
|
public final HypervisorType getHypervisorType() {
|
||||||
return HypervisorType.Hyperv;
|
return HypervisorType.Hyperv;
|
||||||
@ -85,7 +85,7 @@ public class HypervGuru extends HypervisorGuruBase implements HypervisorGuru {
|
|||||||
NicTO[] nics = to.getNics();
|
NicTO[] nics = to.getNics();
|
||||||
|
|
||||||
// reserve extra NICs
|
// reserve extra NICs
|
||||||
NicTO[] expandedNics = new NicTO[nics.length + _hypervMgr.getRouterExtraPublicNics()];
|
NicTO[] expandedNics = new NicTO[MaxNicSupported];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int deviceId = -1;
|
int deviceId = -1;
|
||||||
for(i = 0; i < nics.length; i++) {
|
for(i = 0; i < nics.length; i++) {
|
||||||
@ -97,8 +97,9 @@ public class HypervGuru extends HypervisorGuruBase implements HypervisorGuru {
|
|||||||
|
|
||||||
long networkId = publicNicProfile.getNetworkId();
|
long networkId = publicNicProfile.getNetworkId();
|
||||||
NetworkVO network = _networkDao.findById(networkId);
|
NetworkVO network = _networkDao.findById(networkId);
|
||||||
|
// for Hyperv Hot Nic plug is not supported and it will support upto 8 nics.
|
||||||
for(; i < nics.length + _hypervMgr.getRouterExtraPublicNics(); i++) {
|
// creating the VR with extra nics (actual nics(3) + extra nics) will be 8
|
||||||
|
for(; i < MaxNicSupported; i++) {
|
||||||
NicTO nicTo = new NicTO();
|
NicTO nicTo = new NicTO();
|
||||||
nicTo.setDeviceId(deviceId++);
|
nicTo.setDeviceId(deviceId++);
|
||||||
nicTo.setBroadcastType(publicNicProfile.getBroadcastType());
|
nicTo.setBroadcastType(publicNicProfile.getBroadcastType());
|
||||||
|
|||||||
@ -83,6 +83,7 @@ import com.cloud.agent.api.NetworkUsageCommand;
|
|||||||
import com.cloud.agent.api.PingCommand;
|
import com.cloud.agent.api.PingCommand;
|
||||||
import com.cloud.agent.api.PingRoutingCommand;
|
import com.cloud.agent.api.PingRoutingCommand;
|
||||||
import com.cloud.agent.api.PingTestCommand;
|
import com.cloud.agent.api.PingTestCommand;
|
||||||
|
import com.cloud.agent.api.SetupGuestNetworkCommand;
|
||||||
import com.cloud.agent.api.StartCommand;
|
import com.cloud.agent.api.StartCommand;
|
||||||
import com.cloud.agent.api.StartupCommand;
|
import com.cloud.agent.api.StartupCommand;
|
||||||
import com.cloud.agent.api.StartupRoutingCommand;
|
import com.cloud.agent.api.StartupRoutingCommand;
|
||||||
@ -98,6 +99,7 @@ import com.cloud.agent.api.routing.DnsMasqConfigCommand;
|
|||||||
import com.cloud.agent.api.routing.IpAliasTO;
|
import com.cloud.agent.api.routing.IpAliasTO;
|
||||||
import com.cloud.agent.api.routing.IpAssocAnswer;
|
import com.cloud.agent.api.routing.IpAssocAnswer;
|
||||||
import com.cloud.agent.api.routing.IpAssocCommand;
|
import com.cloud.agent.api.routing.IpAssocCommand;
|
||||||
|
import com.cloud.agent.api.routing.IpAssocVpcCommand;
|
||||||
import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
|
import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
|
||||||
import com.cloud.agent.api.routing.NetworkElementCommand;
|
import com.cloud.agent.api.routing.NetworkElementCommand;
|
||||||
import com.cloud.agent.api.routing.RemoteAccessVpnCfgCommand;
|
import com.cloud.agent.api.routing.RemoteAccessVpnCfgCommand;
|
||||||
@ -105,6 +107,7 @@ import com.cloud.agent.api.routing.SavePasswordCommand;
|
|||||||
import com.cloud.agent.api.routing.SetFirewallRulesAnswer;
|
import com.cloud.agent.api.routing.SetFirewallRulesAnswer;
|
||||||
import com.cloud.agent.api.routing.SetFirewallRulesCommand;
|
import com.cloud.agent.api.routing.SetFirewallRulesCommand;
|
||||||
import com.cloud.agent.api.routing.SetMonitorServiceCommand;
|
import com.cloud.agent.api.routing.SetMonitorServiceCommand;
|
||||||
|
import com.cloud.agent.api.routing.SetNetworkACLCommand;
|
||||||
import com.cloud.agent.api.routing.SetPortForwardingRulesAnswer;
|
import com.cloud.agent.api.routing.SetPortForwardingRulesAnswer;
|
||||||
import com.cloud.agent.api.routing.SetPortForwardingRulesCommand;
|
import com.cloud.agent.api.routing.SetPortForwardingRulesCommand;
|
||||||
import com.cloud.agent.api.routing.SetSourceNatAnswer;
|
import com.cloud.agent.api.routing.SetSourceNatAnswer;
|
||||||
@ -119,9 +122,12 @@ import com.cloud.agent.api.routing.VpnUsersCfgCommand;
|
|||||||
import com.cloud.agent.api.to.DhcpTO;
|
import com.cloud.agent.api.to.DhcpTO;
|
||||||
import com.cloud.agent.api.to.FirewallRuleTO;
|
import com.cloud.agent.api.to.FirewallRuleTO;
|
||||||
import com.cloud.agent.api.to.IpAddressTO;
|
import com.cloud.agent.api.to.IpAddressTO;
|
||||||
|
import com.cloud.agent.api.to.NicTO;
|
||||||
import com.cloud.agent.api.to.PortForwardingRuleTO;
|
import com.cloud.agent.api.to.PortForwardingRuleTO;
|
||||||
import com.cloud.agent.api.to.StaticNatRuleTO;
|
import com.cloud.agent.api.to.StaticNatRuleTO;
|
||||||
import com.cloud.agent.api.to.VirtualMachineTO;
|
import com.cloud.agent.api.to.VirtualMachineTO;
|
||||||
|
import com.cloud.agent.resource.virtualnetwork.VirtualRouterDeployer;
|
||||||
|
import com.cloud.agent.resource.virtualnetwork.VirtualRoutingResource;
|
||||||
import com.cloud.dc.DataCenter.NetworkType;
|
import com.cloud.dc.DataCenter.NetworkType;
|
||||||
import com.cloud.exception.InternalErrorException;
|
import com.cloud.exception.InternalErrorException;
|
||||||
import com.cloud.host.Host.Type;
|
import com.cloud.host.Host.Type;
|
||||||
@ -135,6 +141,7 @@ import com.cloud.network.rules.FirewallRule;
|
|||||||
import com.cloud.resource.ServerResource;
|
import com.cloud.resource.ServerResource;
|
||||||
import com.cloud.resource.ServerResourceBase;
|
import com.cloud.resource.ServerResourceBase;
|
||||||
import com.cloud.serializer.GsonHelper;
|
import com.cloud.serializer.GsonHelper;
|
||||||
|
import com.cloud.utils.ExecutionResult;
|
||||||
import com.cloud.utils.Pair;
|
import com.cloud.utils.Pair;
|
||||||
import com.cloud.utils.StringUtils;
|
import com.cloud.utils.StringUtils;
|
||||||
import com.cloud.utils.net.NetUtils;
|
import com.cloud.utils.net.NetUtils;
|
||||||
@ -148,7 +155,7 @@ import com.cloud.vm.VirtualMachineName;
|
|||||||
* Implementation of dummy resource to be returned from discoverer.
|
* Implementation of dummy resource to be returned from discoverer.
|
||||||
**/
|
**/
|
||||||
@Local(value = ServerResource.class)
|
@Local(value = ServerResource.class)
|
||||||
public class HypervDirectConnectResource extends ServerResourceBase implements ServerResource {
|
public class HypervDirectConnectResource extends ServerResourceBase implements ServerResource, VirtualRouterDeployer {
|
||||||
public static final int DEFAULT_AGENT_PORT = 8250;
|
public static final int DEFAULT_AGENT_PORT = 8250;
|
||||||
public static final String HOST_VM_STATE_REPORT_COMMAND = "org.apache.cloudstack.HostVmStateReportCommand";
|
public static final String HOST_VM_STATE_REPORT_COMMAND = "org.apache.cloudstack.HostVmStateReportCommand";
|
||||||
private static final Logger s_logger = Logger.getLogger(HypervDirectConnectResource.class.getName());
|
private static final Logger s_logger = Logger.getLogger(HypervDirectConnectResource.class.getName());
|
||||||
@ -178,6 +185,7 @@ public class HypervDirectConnectResource extends ServerResourceBase implements S
|
|||||||
private static HypervManager s_hypervMgr;
|
private static HypervManager s_hypervMgr;
|
||||||
@Inject
|
@Inject
|
||||||
HypervManager _hypervMgr;
|
HypervManager _hypervMgr;
|
||||||
|
protected VirtualRoutingResource _vrResource;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
void init() {
|
void init() {
|
||||||
@ -421,8 +429,9 @@ public class HypervDirectConnectResource extends ServerResourceBase implements S
|
|||||||
s_logger.error(errMsg, e);
|
s_logger.error(errMsg, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (cmd instanceof NetworkElementCommand) {
|
||||||
if (clazz == CheckSshCommand.class) {
|
return _vrResource.executeRequest((NetworkElementCommand)cmd);
|
||||||
|
}if (clazz == CheckSshCommand.class) {
|
||||||
answer = execute((CheckSshCommand)cmd);
|
answer = execute((CheckSshCommand)cmd);
|
||||||
} else if (clazz == GetDomRVersionCmd.class) {
|
} else if (clazz == GetDomRVersionCmd.class) {
|
||||||
answer = execute((GetDomRVersionCmd)cmd);
|
answer = execute((GetDomRVersionCmd)cmd);
|
||||||
@ -499,6 +508,203 @@ public class HypervDirectConnectResource extends ServerResourceBase implements S
|
|||||||
}
|
}
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExecutionResult executeInVR(String routerIP, String script, String args) {
|
||||||
|
Pair<Boolean, String> result;
|
||||||
|
|
||||||
|
//TODO: Password should be masked, cannot output to log directly
|
||||||
|
if (s_logger.isDebugEnabled()) {
|
||||||
|
s_logger.debug("Run command on VR: " + routerIP + ", script: " + script + " with args: " + args);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = SshHelper.sshExecute(routerIP, DEFAULT_DOMR_SSHPORT, "root", getSystemVMKeyFile(), null, "/opt/cloud/bin/" + script + " " + args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = "Command failed due to " + e ;
|
||||||
|
s_logger.error(msg);
|
||||||
|
result = new Pair<Boolean, String>(false, msg);
|
||||||
|
}
|
||||||
|
if (s_logger.isDebugEnabled()) {
|
||||||
|
s_logger.debug(script + " execution result: " + result.first().toString());
|
||||||
|
}
|
||||||
|
return new ExecutionResult(result.first(), result.second());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExecutionResult createFileInVR(String routerIp, String filePath, String fileName, String content) {
|
||||||
|
File keyFile = getSystemVMKeyFile();
|
||||||
|
try {
|
||||||
|
SshHelper.scpTo(routerIp, 3922, "root", keyFile, null, filePath, content.getBytes(), fileName, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
s_logger.warn("Fail to create file " + filePath + fileName + " in VR " + routerIp, e);
|
||||||
|
return new ExecutionResult(false, e.getMessage());
|
||||||
|
}
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExecutionResult prepareCommand(NetworkElementCommand cmd) {
|
||||||
|
//Update IP used to access router
|
||||||
|
cmd.setRouterAccessIp(getRouterSshControlIp(cmd));
|
||||||
|
assert cmd.getRouterAccessIp() != null;
|
||||||
|
|
||||||
|
if (cmd instanceof IpAssocVpcCommand) {
|
||||||
|
return prepareNetworkElementCommand((IpAssocVpcCommand)cmd);
|
||||||
|
} else if (cmd instanceof IpAssocCommand) {
|
||||||
|
return prepareNetworkElementCommand((IpAssocCommand)cmd);
|
||||||
|
} else if (cmd instanceof SetSourceNatCommand) {
|
||||||
|
return prepareNetworkElementCommand((SetSourceNatCommand)cmd);
|
||||||
|
} else if (cmd instanceof SetupGuestNetworkCommand) {
|
||||||
|
return prepareNetworkElementCommand((SetupGuestNetworkCommand)cmd);
|
||||||
|
} else if (cmd instanceof SetNetworkACLCommand) {
|
||||||
|
return prepareNetworkElementCommand((SetNetworkACLCommand)cmd);
|
||||||
|
}
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExecutionResult prepareNetworkElementCommand(IpAssocCommand cmd) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
IpAddressTO[] ips = cmd.getIpAddresses();
|
||||||
|
String routerName = cmd.getAccessDetail(NetworkElementCommand.ROUTER_NAME);
|
||||||
|
String controlIp = getRouterSshControlIp(cmd);
|
||||||
|
|
||||||
|
for (IpAddressTO ip : ips) {
|
||||||
|
/**
|
||||||
|
* TODO support other networks
|
||||||
|
*/
|
||||||
|
URI broadcastUri = BroadcastDomainType.fromString(ip.getBroadcastUri());
|
||||||
|
if (BroadcastDomainType.getSchemeValue(broadcastUri) != BroadcastDomainType.Vlan) {
|
||||||
|
throw new InternalErrorException("Unable to assign a public IP to a VIF on network " + ip.getBroadcastUri());
|
||||||
|
}
|
||||||
|
int vlanId = Integer.parseInt(BroadcastDomainType.getValue(broadcastUri));
|
||||||
|
int publicNicInfo = -1;
|
||||||
|
publicNicInfo = getVmNics(routerName, vlanId);
|
||||||
|
|
||||||
|
boolean addVif = false;
|
||||||
|
if (ip.isAdd() && publicNicInfo == -1) {
|
||||||
|
if (s_logger.isDebugEnabled()) {
|
||||||
|
s_logger.debug("Plug new NIC to associate" + controlIp + " to " + ip.getPublicIp());
|
||||||
|
}
|
||||||
|
addVif = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addVif) {
|
||||||
|
Pair<Integer, String> nicdevice = findRouterFreeEthDeviceIndex(controlIp);
|
||||||
|
publicNicInfo = nicdevice.first();
|
||||||
|
if (publicNicInfo > 0) {
|
||||||
|
modifyNicVlan(routerName, vlanId, nicdevice.second());
|
||||||
|
// After modifying the vnic on VR, check the VR VNics config in the host and get the device position
|
||||||
|
publicNicInfo = getVmNics(routerName, vlanId);
|
||||||
|
// As a new nic got activated in the VR. add the entry in the NIC's table.
|
||||||
|
networkUsage(controlIp, "addVif", "eth" + publicNicInfo);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// we didn't find any eth device available in VR to configure the ip range with new VLAN
|
||||||
|
String msg = "No Nic is available on DomR VIF to associate/disassociate IP with.";
|
||||||
|
s_logger.error(msg);
|
||||||
|
throw new InternalErrorException(msg);
|
||||||
|
}
|
||||||
|
ip.setNicDevId(publicNicInfo);
|
||||||
|
ip.setNewNic(addVif);
|
||||||
|
} else {
|
||||||
|
ip.setNicDevId(publicNicInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
s_logger.error("Unexpected exception: " + e.toString() + " will shortcut rest of IPAssoc commands", e);
|
||||||
|
return new ExecutionResult(false, e.toString());
|
||||||
|
}
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ExecutionResult prepareNetworkElementCommand(SetupGuestNetworkCommand cmd) {
|
||||||
|
NicTO nic = cmd.getNic();
|
||||||
|
String routerIp = getRouterSshControlIp(cmd);
|
||||||
|
String domrName =
|
||||||
|
cmd.getAccessDetail(NetworkElementCommand.ROUTER_NAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int ethDeviceNum = findRouterEthDeviceIndex(domrName, routerIp,
|
||||||
|
nic.getMac());
|
||||||
|
nic.setDeviceId(ethDeviceNum);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = "Prepare SetupGuestNetwork failed due to " + e.toString();
|
||||||
|
s_logger.warn(msg, e);
|
||||||
|
return new ExecutionResult(false, msg);
|
||||||
|
}
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ExecutionResult prepareNetworkElementCommand(IpAssocVpcCommand cmd) {
|
||||||
|
String routerName = cmd.getAccessDetail(NetworkElementCommand.ROUTER_NAME);
|
||||||
|
String routerIp = getRouterSshControlIp(cmd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
IpAddressTO[] ips = cmd.getIpAddresses();
|
||||||
|
for (IpAddressTO ip : ips) {
|
||||||
|
|
||||||
|
int ethDeviceNum = findRouterEthDeviceIndex(routerName, routerIp, ip.getVifMacAddress());
|
||||||
|
if (ethDeviceNum < 0) {
|
||||||
|
if (ip.isAdd()) {
|
||||||
|
throw new InternalErrorException("Failed to find DomR VIF to associate/disassociate IP with.");
|
||||||
|
} else {
|
||||||
|
s_logger.debug("VIF to deassociate IP with does not exist, return success");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ip.setNicDevId(ethDeviceNum);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
s_logger.error("Prepare Ip Assoc failure on applying one ip due to exception: ", e);
|
||||||
|
return new ExecutionResult(false, e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ExecutionResult prepareNetworkElementCommand(SetSourceNatCommand cmd) {
|
||||||
|
String routerName = cmd.getAccessDetail(NetworkElementCommand.ROUTER_NAME);
|
||||||
|
String routerIp = getRouterSshControlIp(cmd);
|
||||||
|
IpAddressTO pubIp = cmd.getIpAddress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
int ethDeviceNum = findRouterEthDeviceIndex(routerName, routerIp, pubIp.getVifMacAddress());
|
||||||
|
pubIp.setNicDevId(ethDeviceNum);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = "Prepare Ip SNAT failure due to " + e.toString();
|
||||||
|
s_logger.error(msg, e);
|
||||||
|
return new ExecutionResult(false, e.toString());
|
||||||
|
}
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExecutionResult prepareNetworkElementCommand(SetNetworkACLCommand cmd) {
|
||||||
|
NicTO nic = cmd.getNic();
|
||||||
|
String routerName =
|
||||||
|
cmd.getAccessDetail(NetworkElementCommand.ROUTER_NAME);
|
||||||
|
String routerIp = getRouterSshControlIp(cmd);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int ethDeviceNum = findRouterEthDeviceIndex(routerName, routerIp,
|
||||||
|
nic.getMac());
|
||||||
|
nic.setDeviceId(ethDeviceNum);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = "Prepare SetNetworkACL failed due to " + e.toString();
|
||||||
|
s_logger.error(msg, e);
|
||||||
|
return new ExecutionResult(false, msg);
|
||||||
|
}
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExecutionResult cleanupCommand(NetworkElementCommand cmd) {
|
||||||
|
return new ExecutionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
protected Answer execute(final RemoteAccessVpnCfgCommand cmd) {
|
protected Answer execute(final RemoteAccessVpnCfgCommand cmd) {
|
||||||
String controlIp = getRouterSshControlIp(cmd);
|
String controlIp = getRouterSshControlIp(cmd);
|
||||||
StringBuffer argsBuf = new StringBuffer();
|
StringBuffer argsBuf = new StringBuffer();
|
||||||
@ -1926,6 +2132,10 @@ public class HypervDirectConnectResource extends ServerResourceBase implements S
|
|||||||
_configureCalled = true;
|
_configureCalled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_vrResource = new VirtualRoutingResource(this);
|
||||||
|
if (!_vrResource.configure(name, new HashMap<String, Object>())) {
|
||||||
|
throw new ConfigurationException("Unable to configure VirtualRoutingResource");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1221,10 +1221,6 @@ start() {
|
|||||||
patch_log4j
|
patch_log4j
|
||||||
parse_cmd_line
|
parse_cmd_line
|
||||||
change_password
|
change_password
|
||||||
if [ "$hyp" == "hyperv" ]; then
|
|
||||||
# eject the systemvm.iso
|
|
||||||
eject
|
|
||||||
fi
|
|
||||||
case $TYPE in
|
case $TYPE in
|
||||||
router)
|
router)
|
||||||
[ "$NAME" == "" ] && NAME=router
|
[ "$NAME" == "" ] && NAME=router
|
||||||
@ -1259,6 +1255,10 @@ start() {
|
|||||||
setup_default;
|
setup_default;
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
if [ "$hyp" == "hyperv" ]; then
|
||||||
|
# eject the systemvm.iso
|
||||||
|
eject
|
||||||
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user