mirror of https://github.com/apache/jclouds.git
Merge branch 'master' of https://github.com/andreaturli/jclouds
* 'master' of https://github.com/andreaturli/jclouds: update TODO; removed Datacenter class clone operation added
This commit is contained in:
commit
a23c2dd815
|
@ -1 +1 @@
|
||||||
runNodesWithTag: when ask for more than 1 node, cloning step fails cause of concurrent access to the originale virtual disk to be cloned.
|
runNodesWithTag: pass VirtualMachineRelocateSpec using Template?
|
|
@ -1,56 +0,0 @@
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Copyright (C) 2010 Cloud Conscious, LLC. <info@cloudconscious.com>
|
|
||||||
*
|
|
||||||
* ====================================================================
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
* ====================================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.jclouds.vi;
|
|
||||||
|
|
||||||
import com.google.common.base.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This would be replaced with the real java object related to the underlying data center
|
|
||||||
*
|
|
||||||
* @author Adrian Cole
|
|
||||||
*/
|
|
||||||
public class Datacenter {
|
|
||||||
|
|
||||||
public int id;
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
public Datacenter(int id, String name) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hashCode(id, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object that) {
|
|
||||||
if (that == null)
|
|
||||||
return false;
|
|
||||||
return Objects.equal(this.toString(), that.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return Objects.toStringHelper(this).add("id", id).add("name", name).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -106,10 +106,10 @@ public class ViComputeServiceContextModule
|
||||||
protected TemplateBuilder provideTemplate(Injector injector, TemplateBuilder template) {
|
protected TemplateBuilder provideTemplate(Injector injector, TemplateBuilder template) {
|
||||||
// String domainDir = injector.getInstance(Key.get(String.class,
|
// String domainDir = injector.getInstance(Key.get(String.class,
|
||||||
// Names.named(PROPERTY_LIBVIRT_DOMAIN_DIR)));
|
// Names.named(PROPERTY_LIBVIRT_DOMAIN_DIR)));
|
||||||
String domainDir = "";
|
// String domainDir = "";
|
||||||
String hardwareId = searchForHardwareIdInDomainDir(domainDir);
|
// String hardwareId = searchForHardwareIdInDomainDir(domainDir);
|
||||||
String image = searchForImageIdInDomainDir(domainDir);
|
// String image = searchForImageIdInDomainDir(domainDir);
|
||||||
return template.hardwareId(hardwareId).imageId(image);
|
return template.hardwareId("vm-1221").imageId("winNetEnterprise64Guest");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String searchForImageIdInDomainDir(String domainDir) {
|
private String searchForImageIdInDomainDir(String domainDir) {
|
||||||
|
|
|
@ -36,17 +36,39 @@ import org.jclouds.vi.Image;
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.vmware.vim25.CustomizationAdapterMapping;
|
||||||
|
import com.vmware.vim25.CustomizationDhcpIpGenerator;
|
||||||
|
import com.vmware.vim25.CustomizationFixedName;
|
||||||
|
import com.vmware.vim25.CustomizationGlobalIPSettings;
|
||||||
|
import com.vmware.vim25.CustomizationGuiUnattended;
|
||||||
|
import com.vmware.vim25.CustomizationIPSettings;
|
||||||
|
import com.vmware.vim25.CustomizationIdentification;
|
||||||
|
import com.vmware.vim25.CustomizationIdentitySettings;
|
||||||
|
import com.vmware.vim25.CustomizationLicenseDataMode;
|
||||||
|
import com.vmware.vim25.CustomizationLicenseFilePrintData;
|
||||||
|
import com.vmware.vim25.CustomizationPassword;
|
||||||
|
import com.vmware.vim25.CustomizationSpec;
|
||||||
|
import com.vmware.vim25.CustomizationSysprep;
|
||||||
|
import com.vmware.vim25.CustomizationUserData;
|
||||||
|
import com.vmware.vim25.CustomizationWinOptions;
|
||||||
import com.vmware.vim25.InvalidProperty;
|
import com.vmware.vim25.InvalidProperty;
|
||||||
import com.vmware.vim25.RuntimeFault;
|
import com.vmware.vim25.RuntimeFault;
|
||||||
|
import com.vmware.vim25.VirtualMachineCloneSpec;
|
||||||
|
import com.vmware.vim25.VirtualMachineRelocateSpec;
|
||||||
import com.vmware.vim25.mo.Datacenter;
|
import com.vmware.vim25.mo.Datacenter;
|
||||||
|
import com.vmware.vim25.mo.Datastore;
|
||||||
import com.vmware.vim25.mo.Folder;
|
import com.vmware.vim25.mo.Folder;
|
||||||
|
import com.vmware.vim25.mo.HostDatastoreBrowser;
|
||||||
|
import com.vmware.vim25.mo.HostSystem;
|
||||||
import com.vmware.vim25.mo.InventoryNavigator;
|
import com.vmware.vim25.mo.InventoryNavigator;
|
||||||
import com.vmware.vim25.mo.ManagedEntity;
|
import com.vmware.vim25.mo.ManagedEntity;
|
||||||
|
import com.vmware.vim25.mo.ResourcePool;
|
||||||
import com.vmware.vim25.mo.ServiceInstance;
|
import com.vmware.vim25.mo.ServiceInstance;
|
||||||
|
import com.vmware.vim25.mo.Task;
|
||||||
import com.vmware.vim25.mo.VirtualMachine;
|
import com.vmware.vim25.mo.VirtualMachine;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* defines the connection between the {@link Libvirt} implementation and the jclouds
|
* defines the connection between the {@link VI} implementation and the jclouds
|
||||||
* {@link ComputeService}
|
* {@link ComputeService}
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@ -54,6 +76,10 @@ import com.vmware.vim25.mo.VirtualMachine;
|
||||||
public class ViComputeServiceAdapter implements ComputeServiceAdapter<VirtualMachine, VirtualMachine, Image, Datacenter> {
|
public class ViComputeServiceAdapter implements ComputeServiceAdapter<VirtualMachine, VirtualMachine, Image, Datacenter> {
|
||||||
|
|
||||||
private final ServiceInstance client;
|
private final ServiceInstance client;
|
||||||
|
private String resourcePoolName = "";
|
||||||
|
private String vmwareHostName = "";
|
||||||
|
private String datastoreName = "";
|
||||||
|
private String vmClonedName = "MyWinClone";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ViComputeServiceAdapter(ServiceInstance client) {
|
public ViComputeServiceAdapter(ServiceInstance client) {
|
||||||
|
@ -64,59 +90,159 @@ public class ViComputeServiceAdapter implements ComputeServiceAdapter<VirtualMac
|
||||||
public VirtualMachine runNodeWithTagAndNameAndStoreCredentials(String tag,
|
public VirtualMachine runNodeWithTagAndNameAndStoreCredentials(String tag,
|
||||||
String name, Template template,
|
String name, Template template,
|
||||||
Map<String, Credentials> credentialStore) {
|
Map<String, Credentials> credentialStore) {
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
@Override
|
|
||||||
public Domain runNodeWithTagAndNameAndStoreCredentials(String tag, String name, Template template,
|
|
||||||
Map<String, Credentials> credentialStore) {
|
|
||||||
try {
|
try {
|
||||||
String domainName = tag;
|
Folder rootFolder = client.getRootFolder();
|
||||||
Domain domain = client.domainLookupByName(domainName);
|
|
||||||
XMLBuilder builder = XMLBuilder.parse(new InputSource(new StringReader(domain.getXMLDesc(0))));
|
|
||||||
Document doc = builder.getDocument();
|
|
||||||
String xpathString = "//devices/disk[@device='disk']/source/@file";
|
|
||||||
XPathExpression expr = XPathFactory.newInstance().newXPath().compile(xpathString);
|
|
||||||
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
|
|
||||||
String diskFileName = nodes.item(0).getNodeValue();
|
|
||||||
StorageVol storageVol = client.storageVolLookupByPath(diskFileName);
|
|
||||||
|
|
||||||
// cloning volume
|
VirtualMachine from = (VirtualMachine) new InventoryNavigator(
|
||||||
String poolName = storageVol.storagePoolLookupByVolume().getName();
|
rootFolder).searchManagedEntity("VirtualMachine", tag);
|
||||||
StoragePool storagePool = client.storagePoolLookupByName(poolName);
|
|
||||||
StorageVol clonedVol = null;
|
if (from == null) {
|
||||||
boolean cloned = false;
|
client.getServerConnection().logout();
|
||||||
int retry = 0;
|
return null;
|
||||||
while(!cloned && retry<10) {
|
|
||||||
try {
|
|
||||||
clonedVol = cloneVolume(storagePool, storageVol);
|
|
||||||
cloned = true;
|
|
||||||
} catch (LibvirtException e) {
|
|
||||||
retry++;
|
|
||||||
Thread.sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// define Domain
|
|
||||||
String xmlFinal = generateClonedDomainXML(domain.getXMLDesc(0), clonedVol);
|
VirtualMachineCloneSpec cloneSpec = new VirtualMachineCloneSpec();
|
||||||
Domain newDomain = client.domainDefineXML(xmlFinal);
|
VirtualMachineRelocateSpec virtualMachineRelocateSpec = new VirtualMachineRelocateSpec();
|
||||||
newDomain.create();
|
if (!vmwareHostName.equals("") && !datastoreName.equals("") && !resourcePoolName.equals("")) {
|
||||||
// store the credentials so that later functions can use them
|
|
||||||
credentialStore.put(domain.getUUIDString() + "", new Credentials("identity", "credential"));
|
ResourcePool rp = (ResourcePool) new InventoryNavigator(rootFolder)
|
||||||
return newDomain;
|
.searchManagedEntity("ResourcePool", resourcePoolName);
|
||||||
} catch (LibvirtException e) {
|
|
||||||
|
if(rp == null)
|
||||||
|
throw new Exception("The resourcePool specified '" + resourcePoolName + "' doesn't exist");
|
||||||
|
virtualMachineRelocateSpec.setPool(rp.getMOR());
|
||||||
|
|
||||||
|
|
||||||
|
Datastore ds = (Datastore) new InventoryNavigator(rootFolder)
|
||||||
|
.searchManagedEntity("Datastore", datastoreName);
|
||||||
|
|
||||||
|
HostSystem host = null;
|
||||||
|
host = (HostSystem) new InventoryNavigator(rootFolder)
|
||||||
|
.searchManagedEntity("HostSystem", vmwareHostName);
|
||||||
|
|
||||||
|
HostDatastoreBrowser hdb = host.getDatastoreBrowser();
|
||||||
|
|
||||||
|
if(ds == null)
|
||||||
|
throw new Exception("Cannot relocate this cloned machine to the specified datastore '" + datastoreName + "'");
|
||||||
|
Datastore dsFound = null;
|
||||||
|
Datastore[] dsArray = hdb.getDatastores();
|
||||||
|
for (Datastore d : dsArray) {
|
||||||
|
if(d.getName().equalsIgnoreCase(ds.getName()))
|
||||||
|
dsFound = d;
|
||||||
|
}
|
||||||
|
if(dsFound == null)
|
||||||
|
throw new Exception("Cannot relocate this cloned machine to the specified datastore '" + datastoreName + "'");
|
||||||
|
virtualMachineRelocateSpec.setDatastore(dsFound.getMOR());
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomizationSpec custSpec = new CustomizationSpec();
|
||||||
|
|
||||||
|
CustomizationAdapterMapping cam = new CustomizationAdapterMapping();
|
||||||
|
CustomizationIPSettings cip = new CustomizationIPSettings();
|
||||||
|
cip.setIp(new CustomizationDhcpIpGenerator());
|
||||||
|
cam.setAdapter(cip);
|
||||||
|
|
||||||
|
|
||||||
|
// IP customization
|
||||||
|
// CustomizationAdapterMapping[] custAdapMapList = new CustomizationAdapterMapping[1];
|
||||||
|
// CustomizationAdapterMapping custAdapMap = new CustomizationAdapterMapping();
|
||||||
|
// CustomizationIPSettings custIPSettings = new CustomizationIPSettings();
|
||||||
|
// CustomizationFixedIp custFixedIp = new CustomizationFixedIp();
|
||||||
|
// custFixedIp.setIpAddress(ipAddress);
|
||||||
|
// custIPSettings.setIp(custFixedIp);
|
||||||
|
// custAdapMap.setAdapter(custIPSettings);
|
||||||
|
// custAdapMapList[0] = custAdapMap;
|
||||||
|
// custSpec.setNicSettingMap(custAdapMapList);
|
||||||
|
|
||||||
|
CustomizationGlobalIPSettings custGlobalIPSetting = new CustomizationGlobalIPSettings();
|
||||||
|
|
||||||
|
|
||||||
|
CustomizationIdentitySettings custIdentitySet = new CustomizationIdentitySettings();
|
||||||
|
|
||||||
|
// sysprep customization
|
||||||
|
CustomizationSysprep custSysprep = new CustomizationSysprep();
|
||||||
|
|
||||||
|
CustomizationGuiUnattended guiUnattended = new CustomizationGuiUnattended();
|
||||||
|
guiUnattended.setAutoLogon(false);
|
||||||
|
guiUnattended.setAutoLogonCount(0);
|
||||||
|
guiUnattended.setTimeZone(190);
|
||||||
|
|
||||||
|
|
||||||
|
// user data
|
||||||
|
CustomizationPassword custPasswd = new CustomizationPassword();
|
||||||
|
custPasswd.setPlainText(true);
|
||||||
|
custPasswd.setValue("password");
|
||||||
|
|
||||||
|
CustomizationIdentification custIdentification = new CustomizationIdentification();
|
||||||
|
custIdentification.setDomainAdmin("Administrator");
|
||||||
|
custIdentification.setDomainAdminPassword(custPasswd);
|
||||||
|
custIdentification.setJoinWorkgroup("WORKGROUP");
|
||||||
|
|
||||||
|
CustomizationUserData custUserData = new CustomizationUserData();
|
||||||
|
CustomizationFixedName custFixedName = new CustomizationFixedName();
|
||||||
|
custFixedName.setName("mycomputer");
|
||||||
|
custUserData.setComputerName(custFixedName);
|
||||||
|
custUserData.setFullName("sjain");
|
||||||
|
custUserData.setOrgName("vmware");
|
||||||
|
custUserData.setProductId("PDRXT-M9X8G-898BR-4K427-J2FFY");
|
||||||
|
|
||||||
|
///////
|
||||||
|
CustomizationWinOptions customizationWinOptions = new CustomizationWinOptions();
|
||||||
|
customizationWinOptions.setChangeSID(true);
|
||||||
|
customizationWinOptions.setDeleteAccounts(false);
|
||||||
|
|
||||||
|
CustomizationLicenseFilePrintData custLPD = new CustomizationLicenseFilePrintData();
|
||||||
|
custLPD.setAutoMode(CustomizationLicenseDataMode.perServer);
|
||||||
|
|
||||||
|
custSysprep.setLicenseFilePrintData(custLPD);
|
||||||
|
|
||||||
|
custSysprep.setUserData(custUserData);
|
||||||
|
custSysprep.setGuiUnattended(guiUnattended);
|
||||||
|
custSysprep.setIdentification(custIdentification);
|
||||||
|
|
||||||
|
custSpec.setIdentity(custSysprep);
|
||||||
|
custSpec.setNicSettingMap(new CustomizationAdapterMapping[] {cam});
|
||||||
|
custSpec.setGlobalIPSettings(custGlobalIPSetting);
|
||||||
|
custSpec.setOptions(customizationWinOptions);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cloneSpec.setCustomization(custSpec);
|
||||||
|
|
||||||
|
//location properties
|
||||||
|
cloneSpec.setLocation(virtualMachineRelocateSpec);
|
||||||
|
cloneSpec.setPowerOn(false);
|
||||||
|
cloneSpec.setTemplate(false);
|
||||||
|
|
||||||
|
Task task = from.cloneVM_Task((Folder) from.getParent(), vmClonedName,
|
||||||
|
cloneSpec);
|
||||||
|
|
||||||
|
String result = task.waitForTask();
|
||||||
|
return (VirtualMachine) new InventoryNavigator(
|
||||||
|
rootFolder).searchManagedEntity("VirtualMachine", vmClonedName);
|
||||||
|
} catch (RemoteException e) {
|
||||||
return propogate(e);
|
return propogate(e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return propogate(e);
|
return propogate(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterable<VirtualMachine> listHardwareProfiles() {
|
public Iterable<VirtualMachine> listHardwareProfiles() {
|
||||||
// TODO
|
// TODO
|
||||||
return null;
|
List<VirtualMachine> hardwareProfiles = Lists.newArrayList();
|
||||||
|
try {
|
||||||
|
|
||||||
|
ManagedEntity[] entities = new InventoryNavigator(
|
||||||
|
client.getRootFolder()).searchManagedEntities("VirtualMachine");
|
||||||
|
for (ManagedEntity entity : entities) {
|
||||||
|
hardwareProfiles.add((VirtualMachine) entity);
|
||||||
|
}
|
||||||
|
return hardwareProfiles;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return propogate(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.jclouds.compute.ComputeServiceContextFactory;
|
||||||
import org.jclouds.compute.domain.ComputeMetadata;
|
import org.jclouds.compute.domain.ComputeMetadata;
|
||||||
import org.jclouds.compute.domain.Hardware;
|
import org.jclouds.compute.domain.Hardware;
|
||||||
import org.jclouds.compute.domain.Image;
|
import org.jclouds.compute.domain.Image;
|
||||||
import org.jclouds.compute.domain.NodeMetadata;
|
|
||||||
import org.jclouds.domain.Location;
|
import org.jclouds.domain.Location;
|
||||||
import org.testng.annotations.BeforeClass;
|
import org.testng.annotations.BeforeClass;
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
|
@ -60,37 +59,47 @@ public class ViExperimentLiveTest {
|
||||||
context = new ComputeServiceContextFactory().createContext(new ViComputeServiceContextSpec(
|
context = new ComputeServiceContextFactory().createContext(new ViComputeServiceContextSpec(
|
||||||
endpoint, identity, credential));
|
endpoint, identity, credential));
|
||||||
|
|
||||||
// Set<? extends Location> locations = context.getComputeService().listAssignableLocations();
|
Set<? extends Location> locations = context.getComputeService().listAssignableLocations();
|
||||||
//
|
for (Location location : locations) {
|
||||||
|
System.out.println("location id: " + location.getId() + " - desc: " + location.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
// Set<? extends ComputeMetadata> nodes = context.getComputeService().listNodes();
|
// Set<? extends ComputeMetadata> nodes = context.getComputeService().listNodes();
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Set<? extends Hardware> hardwares = context.getComputeService().listHardwareProfiles();
|
|
||||||
//
|
//
|
||||||
// Set<? extends Image> images = context.getComputeService().listImages();
|
Set<? extends Hardware> hardwares = context.getComputeService().listHardwareProfiles();
|
||||||
|
for (Hardware hardware : hardwares) {
|
||||||
NodeMetadata node = context.getComputeService().getNodeMetadata("provaVM");
|
System.out.println("hardware id: " + hardware.getId() + " - name: " + hardware.getName());
|
||||||
System.out.println(node);
|
}
|
||||||
|
//
|
||||||
|
Set<? extends Image> images = context.getComputeService().listImages();
|
||||||
|
for (Image image : images) {
|
||||||
|
System.out.println("id: " + image.getId() + " - name:" + image.getName());
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// NodeMetadata node = context.getComputeService().getNodeMetadata("MyWinServer");
|
||||||
|
// System.out.println(node);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* We will probably make a default template out of properties at some point You can control
|
* We will probably make a default template out of properties at some point You can control
|
||||||
* the default template via overriding a method in standalonecomputeservicexontextmodule
|
* the default template via overriding a method in standalonecomputeservicexontextmodule
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
/* Template defaultTemplate = context.getComputeService().templateBuilder()
|
Template defaultTemplate = context.getComputeService().templateBuilder()
|
||||||
.hardwareId("d106ae67-5a1b-8f91-b311-83c93bcb0a1f").imageId("1") //.locationId("")
|
.hardwareId("vm-1221").imageId("winNetEnterprise64Guest") //.locationId("")
|
||||||
.build();
|
.build();
|
||||||
Set<? extends NodeMetadata> nodeMetadataSet = context.getComputeService().runNodesWithTag("MyServer", 1);
|
|
||||||
|
Set<? extends NodeMetadata> nodeMetadataSet = context.getComputeService().runNodesWithTag("MyWinServer", 1);
|
||||||
for (NodeMetadata nodeMetadata : nodeMetadataSet) {
|
for (NodeMetadata nodeMetadata : nodeMetadataSet) {
|
||||||
|
|
||||||
// context.getComputeService().suspendNode(nodeMetadata.getId());
|
// context.getComputeService().suspendNode(nodeMetadata.getId());
|
||||||
// context.getComputeService().resumeNode(nodeMetadata.getId());
|
// context.getComputeService().resumeNode(nodeMetadata.getId());
|
||||||
|
|
||||||
context.getComputeService().destroyNode(nodeMetadata.getId());
|
//context.getComputeService().destroyNode(nodeMetadata.getId());
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
*/
|
|
||||||
} finally {
|
} finally {
|
||||||
if (context != null)
|
if (context != null)
|
||||||
context.close();
|
context.close();
|
||||||
|
|
Loading…
Reference in New Issue