diff --git a/api/src/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/com/cloud/agent/api/to/VirtualMachineTO.java index 3e508eea315..c367ec9e07d 100644 --- a/api/src/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/com/cloud/agent/api/to/VirtualMachineTO.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.agent.api.to; +import java.util.List; import java.util.Map; import com.cloud.template.VirtualMachineTemplate.BootloaderType; @@ -62,6 +63,12 @@ public class VirtualMachineTO { NicTO[] nics; GPUDeviceTO gpuDevice; Integer vcpuMaxLimit; + List vmData = null; + + String configDriveLabel = null; + String configDriveIsoRootFolder = null; + String configDriveIsoFile = null; + public VirtualMachineTO(long id, String instanceName, VirtualMachine.Type type, int cpus, Integer speed, long minRam, long maxRam, BootloaderType bootloader, String os, boolean enableHA, boolean limitCpuUse, String vncPassword) { @@ -292,4 +299,36 @@ public class VirtualMachineTO { this.vcpuMaxLimit = vcpuMaxLimit; } + public List getVmData() { + return vmData; + } + + public void setVmData(List vmData) { + this.vmData = vmData; + } + + public String getConfigDriveLabel() { + return configDriveLabel; + } + + public void setConfigDriveLabel(String configDriveLabel) { + this.configDriveLabel = configDriveLabel; + } + + public String getConfigDriveIsoRootFolder() { + return configDriveIsoRootFolder; + } + + public void setConfigDriveIsoRootFolder(String configDriveIsoRootFolder) { + this.configDriveIsoRootFolder = configDriveIsoRootFolder; + } + + public String getConfigDriveIsoFile() { + return configDriveIsoFile; + } + + public void setConfigDriveIsoFile(String configDriveIsoFile) { + this.configDriveIsoFile = configDriveIsoFile; + } + } diff --git a/api/src/com/cloud/network/NetworkModel.java b/api/src/com/cloud/network/NetworkModel.java index 70132d2b1df..780f97d22f4 100644 --- a/api/src/com/cloud/network/NetworkModel.java +++ b/api/src/com/cloud/network/NetworkModel.java @@ -277,4 +277,8 @@ public interface NetworkModel { boolean isNetworkReadyForGc(long networkId); boolean getNetworkEgressDefaultPolicy(Long networkId); + + List generateVmData(String userData, String serviceOffering, String zoneName, + String vmName, long vmId, String publicKey, String password, Boolean isWindows); + } diff --git a/api/src/com/cloud/vm/UserVmService.java b/api/src/com/cloud/vm/UserVmService.java index c0c03356752..2935815a103 100644 --- a/api/src/com/cloud/vm/UserVmService.java +++ b/api/src/com/cloud/vm/UserVmService.java @@ -55,8 +55,13 @@ import com.cloud.template.VirtualMachineTemplate; import com.cloud.user.Account; import com.cloud.uservm.UserVm; import com.cloud.utils.exception.ExecutionException; +import org.apache.cloudstack.framework.config.ConfigKey; public interface UserVmService { + + static final ConfigKey VmConfigDriveLabel = new ConfigKey("Hidden", String.class, "vm.configdrive.label", "config", + "The default lable name for the config drive", false); + /** * Destroys one virtual machine * diff --git a/api/src/com/cloud/vm/VirtualMachineProfile.java b/api/src/com/cloud/vm/VirtualMachineProfile.java index d0fea49ae8d..c092d843b6f 100644 --- a/api/src/com/cloud/vm/VirtualMachineProfile.java +++ b/api/src/com/cloud/vm/VirtualMachineProfile.java @@ -34,9 +34,24 @@ import com.cloud.user.Account; */ public interface VirtualMachineProfile { + List getVmData(); + + void setVmData(List vmData); + + void setConfigDriveLabel(String configDriveLabel); + + String getConfigDriveIsoRootFolder(); + + void setConfigDriveIsoRootFolder(String configDriveIsoRootFolder); + + String getConfigDriveIsoFile(); + + void setConfigDriveIsoFile(String isoFile); + public static class Param { public static final Param VmPassword = new Param("VmPassword"); + public static final Param VmSshPubKey = new Param("VmSshPubKey"); public static final Param ControlNic = new Param("ControlNic"); public static final Param ReProgramGuestNetworks = new Param("RestartNetwork"); public static final Param PxeSeverType = new Param("PxeSeverType"); diff --git a/engine/components-api/src/com/cloud/vm/VirtualMachineProfileImpl.java b/engine/components-api/src/com/cloud/vm/VirtualMachineProfileImpl.java index 4284a218afc..7a0a07cc6f8 100644 --- a/engine/components-api/src/com/cloud/vm/VirtualMachineProfileImpl.java +++ b/engine/components-api/src/com/cloud/vm/VirtualMachineProfileImpl.java @@ -51,6 +51,13 @@ public class VirtualMachineProfileImpl implements VirtualMachineProfile { VirtualMachine.Type _type; + List vmData = null; + + String configDriveLabel = null; + String configDriveIsoBaseLocation = "/tmp/"; //TODO: Make this location configurable. + String configDriveIsoRootFolder = null; + String configDriveIsoFile = null; + public VirtualMachineProfileImpl(VirtualMachine vm, VirtualMachineTemplate template, ServiceOffering offering, Account owner, Map params) { _vm = vm; _template = template; @@ -255,4 +262,47 @@ public class VirtualMachineProfileImpl implements VirtualMachineProfile { public Float getMemoryOvercommitRatio() { return memoryOvercommitRatio; } + + @Override + public List getVmData() { + return vmData; + } + + @Override + public void setVmData(List vmData) { + this.vmData = vmData; + } + + public String getConfigDriveLabel() { + return configDriveLabel; + } + + @Override + public void setConfigDriveLabel(String configDriveLabel) { + this.configDriveLabel = configDriveLabel; + } + + @Override + public String getConfigDriveIsoRootFolder() { + return configDriveIsoRootFolder; + } + + @Override + public void setConfigDriveIsoRootFolder(String configDriveIsoRootFolder) { + this.configDriveIsoRootFolder = configDriveIsoRootFolder; + } + + public String getConfigDriveIsoBaseLocation() { + return configDriveIsoBaseLocation; + } + + @Override + public String getConfigDriveIsoFile() { + return configDriveIsoFile; + } + + @Override + public void setConfigDriveIsoFile(String isoFile) { + this.configDriveIsoFile = isoFile; + } } diff --git a/plugins/hypervisors/xenserver/src/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java b/plugins/hypervisors/xenserver/src/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java index ef0631e180b..5366927d015 100644 --- a/plugins/hypervisors/xenserver/src/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java +++ b/plugins/hypervisors/xenserver/src/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java @@ -17,7 +17,9 @@ package com.cloud.hypervisor.xenserver.resource; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; @@ -48,6 +50,7 @@ import javax.xml.parsers.ParserConfigurationException; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.apache.xmlrpc.XmlRpcException; import org.w3c.dom.Document; @@ -251,6 +254,10 @@ public abstract class CitrixResourceBase implements ServerResource, HypervisorRe protected VirtualRoutingResource _vrResource; + protected String _configDriveIsopath = "/opt/xensource/packages/configdrive_iso/"; + protected String _configDriveSRName = "ConfigDriveISOs"; + protected String _attachIsoDeviceNum = "3"; + protected int _wait; // Hypervisor specific params with generic value, may need to be overridden // for specific versions @@ -4940,4 +4947,317 @@ public abstract class CitrixResourceBase implements ServerResource, HypervisorRe } } } + + public boolean createAndAttachConfigDriveIsoForVM(Connection conn, VM vm, List vmDataList, String configDriveLabel) throws XenAPIException, XmlRpcException { + + String vmName = vm.getNameLabel(conn); + + // create SR + SR sr = createLocalIsoSR(conn, _configDriveSRName+_host.getIp()); + if (sr == null) { + s_logger.debug("Failed to create local SR for the config drive"); + return false; + } + + s_logger.debug("Creating vm data files in config drive for vm "+vmName); + // 1. create vm data files + if (!createVmdataFiles(vmName, vmDataList, configDriveLabel)) { + s_logger.debug("Failed to create vm data files in config drive for vm "+vmName); + return false; + } + + // 2. copy config drive iso to host + if (!copyConfigDriveIsoToHost(conn, sr, vmName)) { + return false; + } + + // 3. attachIsoToVM + if (!attachConfigDriveIsoToVm(conn, vm)) { + return false; + } + + return true; + } + + public boolean createVmdataFiles(String vmName, List vmDataList, String configDriveLabel) { + + // add vm iso to the isolibrary + String isoPath = "/tmp/"+vmName+"/configDrive/"; + String configDriveName = "cloudstack/"; + + //create folder for the VM + //Remove the folder before creating it. + + try { + deleteLocalFolder("/tmp/"+isoPath); + } catch (IOException e) { + s_logger.debug("Failed to delete the exiting config drive for vm "+vmName+ " "+ e.getMessage()); + } catch (Exception e) { + s_logger.debug("Failed to delete the exiting config drive for vm "+vmName+ " "+ e.getMessage()); + } + + + if (vmDataList != null) { + for (String[] item : vmDataList) { + String dataType = item[0]; + String fileName = item[1]; + String content = item[2]; + + // create file with content in folder + + if (dataType != null && !dataType.isEmpty()) { + //create folder + String folder = isoPath+configDriveName+dataType; + if (folder != null && !folder.isEmpty()) { + File dir = new File(folder); + boolean result = true; + + try { + if (!dir.exists()) { + dir.mkdirs(); + } + }catch (SecurityException ex) { + s_logger.debug("Failed to create dir "+ ex.getMessage()); + return false; + } + + if (result && content != null && !content.isEmpty()) { + try { + File file = new File(folder+"/"+fileName+".txt"); + FileWriter fw = new FileWriter(file.getAbsoluteFile()); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(content); + bw.close(); + s_logger.debug("created file: "+ file + " in folder:"+folder); + } catch (IOException ex) { + s_logger.debug("Failed to create file "+ ex.getMessage()); + return false; + } + } + } + } + } + s_logger.debug("Created the vm data in "+ isoPath); + } + + String s = null; + try { + + String cmd = "mkisofs -iso-level 3 -V "+ configDriveLabel +" -o "+ isoPath+vmName +".iso " + isoPath; + Process p = Runtime.getRuntime().exec(cmd); + + BufferedReader stdInput = new BufferedReader(new + InputStreamReader(p.getInputStream())); + + BufferedReader stdError = new BufferedReader(new + InputStreamReader(p.getErrorStream())); + + // read the output from the command + while ((s = stdInput.readLine()) != null) { + s_logger.debug(s); + } + + // read any errors from the attempted command + while ((s = stdError.readLine()) != null) { + s_logger.debug(s); + } + s_logger.debug(" Created config drive ISO using the command " + cmd +" in the host "+ _host.getIp()); + } catch (IOException e) { + s_logger.debug(e.getMessage()); + return false; + } + + return true; + } + + public boolean copyConfigDriveIsoToHost(Connection conn, SR sr, String vmName) { + + String vmIso = "/tmp/"+vmName+"/configDrive/"+vmName+".iso"; + //scp file into the host + com.trilead.ssh2.Connection sshConnection = new com.trilead.ssh2.Connection(_host.getIp(), 22); + + try { + sshConnection.connect(null, 60000, 60000); + if (!sshConnection.authenticateWithPassword(_username, _password.peek())) { + throw new CloudRuntimeException("Unable to authenticate"); + } + + s_logger.debug("scp config drive iso file "+vmIso +" to host " + _host.getIp() +" path "+_configDriveIsopath); + SCPClient scp = new SCPClient(sshConnection); + String p = "0755"; + + scp.put(vmIso, _configDriveIsopath, p); + sr.scan(conn); + s_logger.debug("copied config drive iso to host " + _host); + } catch (IOException e) { + s_logger.debug("failed to copy configdrive iso " + vmIso + " to host " + _host, e); + return false; + } catch (XmlRpcException e) { + s_logger.debug("Failed to scan config drive iso SR "+ _configDriveSRName+_host.getIp() + " in host "+ _host, e); + return false; + } finally { + sshConnection.close(); + //clean up the config drive files + + String configDir = "/tmp/"+vmName; + try { + deleteLocalFolder(configDir); + s_logger.debug("Successfully cleaned up config drive directory " + configDir + + " after copying it to host "); + } catch (Exception e) { + s_logger.debug("Failed to delete config drive folder :" + configDir + " for VM " + vmName + " " + + e.getMessage()); + } + } + + return true; + } + + public boolean attachConfigDriveIsoToVm(Connection conn, VM vm) throws XenAPIException, XmlRpcException { + + String vmName = vm.getNameLabel(conn); + String isoURL = _configDriveIsopath + vmName+".iso"; + VDI srVdi; + + //1. find the vdi of the iso + //2. find the vbd for the vdi + //3. attach iso to vm + + try { + Set vdis = VDI.getByNameLabel(conn, vmName+".iso"); + if (vdis.isEmpty()) { + throw new CloudRuntimeException("Could not find ISO with URL: " + isoURL); + } + srVdi = vdis.iterator().next(); + + } catch (XenAPIException e) { + s_logger.debug("Unable to get config drive iso: " + isoURL + " due to " + e.toString()); + return false; + } catch (Exception e) { + s_logger.debug("Unable to get config drive iso: " + isoURL + " due to " + e.toString()); + return false; + } + + VBD isoVBD = null; + + // Find the VM's CD-ROM VBD + Set vbds = vm.getVBDs(conn); + for (VBD vbd : vbds) { + Types.VbdType type = vbd.getType(conn); + + VBD.Record vbdr = vbd.getRecord(conn); + + // if the device exists then attach it + if (!vbdr.userdevice.equals(_attachIsoDeviceNum) && type == Types.VbdType.CD) { + isoVBD = vbd; + break; + } + } + + if (isoVBD == null) { + //create vbd + VBD.Record cfgDriveVbdr = new VBD.Record(); + cfgDriveVbdr.VM = vm; + cfgDriveVbdr.empty = true; + cfgDriveVbdr.bootable = false; + cfgDriveVbdr.userdevice = "autodetect"; + cfgDriveVbdr.mode = Types.VbdMode.RO; + cfgDriveVbdr.type = Types.VbdType.CD; + VBD cfgDriveVBD = VBD.create(conn, cfgDriveVbdr); + isoVBD = cfgDriveVBD; + + s_logger.debug("Created CD-ROM VBD for VM: " + vm); + } + + if (isoVBD != null) { + // If an ISO is already inserted, eject it + if (isoVBD.getEmpty(conn) == false) { + isoVBD.eject(conn); + } + + try { + // Insert the new ISO + isoVBD.insert(conn, srVdi); + s_logger.debug("Attached config drive iso to vm " + vmName); + }catch (XmlRpcException ex) { + s_logger.debug("Failed to attach config drive iso to vm " + vmName); + return false; + } + } + + return true; + } + + public SR createLocalIsoSR(Connection conn, String srName) throws XenAPIException, XmlRpcException { + + // if config drive sr already exists then return + SR sr = getSRByNameLabelandHost(conn, _configDriveSRName+_host.getIp()); + + if (sr != null) { + s_logger.debug("Config drive SR already exist, returing it"); + return sr; + } + + try{ + Map deviceConfig = new HashMap(); + + com.trilead.ssh2.Connection sshConnection = new com.trilead.ssh2.Connection(_host.getIp(), 22); + try { + sshConnection.connect(null, 60000, 60000); + if (!sshConnection.authenticateWithPassword(_username, _password.peek())) { + throw new CloudRuntimeException("Unable to authenticate"); + } + + String cmd = "mkdir -p " + _configDriveIsopath; + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, cmd)) { + throw new CloudRuntimeException("Cannot create directory configdrive_iso on XenServer hosts"); + } + } catch (IOException e) { + throw new CloudRuntimeException("Unable to create iso folder", e); + } finally { + sshConnection.close(); + } + s_logger.debug("Created the config drive SR " + srName +" folder path "+ _configDriveIsopath); + + deviceConfig.put("location", _configDriveIsopath); + deviceConfig.put("legacy_mode", "true"); + Host host = Host.getByUuid(conn, _host.getUuid()); + String type = SRType.ISO.toString(); + sr = SR.create(conn, host, deviceConfig, new Long(0), _configDriveIsopath, "iso", type, "iso", false, new HashMap()); + + sr.setNameLabel(conn, srName); + sr.setNameDescription(conn, deviceConfig.get("location")); + + sr.scan(conn); + s_logger.debug("Config drive ISO SR at the path " + _configDriveIsopath +" got created in host " + _host); + return sr; + } catch (XenAPIException e) { + String msg = "createLocalIsoSR failed! mountpoint " + e.toString(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } catch (Exception e) { + String msg = "createLocalIsoSR failed! mountpoint: due to " + e.getMessage(); + s_logger.warn(msg, e); + throw new CloudRuntimeException(msg, e); + } + + } + + + public void deleteLocalFolder(String directory) throws Exception { + if (directory == null || directory.isEmpty()) { + String msg = "Invalid directory path (null/empty) detected. Cannot delete specified directory."; + s_logger.debug(msg); + throw new Exception(msg); + } + + try { + FileUtils.deleteDirectory(new File(directory)); + } catch (IOException e) { + // IOException here means failure to delete. Not swallowing it here to + // let the caller handle with appropriate contextual log message. + throw e; + } + } + } diff --git a/server/src/com/cloud/network/NetworkModelImpl.java b/server/src/com/cloud/network/NetworkModelImpl.java index b867b6422f3..a49da68d90c 100644 --- a/server/src/com/cloud/network/NetworkModelImpl.java +++ b/server/src/com/cloud/network/NetworkModelImpl.java @@ -19,6 +19,8 @@ package com.cloud.network; import java.math.BigInteger; import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -32,6 +34,8 @@ import javax.ejb.Local; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.StringUtils; +import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.apache.cloudstack.acl.ControlledEntity.ACLType; @@ -2281,4 +2285,55 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel { throw ex; } } + + @Override + public List generateVmData(String userData, String serviceOffering, String zoneName, + String vmName, long vmId, String publicKey, String password, Boolean isWindows) { + final List vmData = new ArrayList(); + + if (userData != null) { + vmData.add(new String[]{"userdata", "user-data", new String(Base64.decodeBase64(userData.getBytes()))}); + } + vmData.add(new String[]{"metadata", "service-offering", StringUtils.unicodeEscape(serviceOffering)}); + vmData.add(new String[]{"metadata", "availability-zone", StringUtils.unicodeEscape(zoneName)}); + vmData.add(new String[]{"metadata", "local-hostname", StringUtils.unicodeEscape(vmName)}); + vmData.add(new String[]{"metadata", "instance-id", vmName}); + vmData.add(new String[]{"metadata", "vm-id", String.valueOf(vmId)}); + vmData.add(new String[]{"metadata", "public-keys", publicKey}); + + String cloudIdentifier = _configDao.getValue("cloud.identifier"); + if (cloudIdentifier == null) { + cloudIdentifier = ""; + } else { + cloudIdentifier = "CloudStack-{" + cloudIdentifier + "}"; + } + vmData.add(new String[]{"metadata", "cloud-identifier", cloudIdentifier}); + + if (password != null && !password.isEmpty() && !password.equals("saved_password")) { + + // Here we are calculating MD5 checksum to reduce the over head of calculating MD5 checksum + // in windows VM in password reset script. + + if (isWindows) { + MessageDigest md5 = null; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unexpected exception " + e.getMessage(), e); + throw new CloudRuntimeException("Unable to get MD5 MessageDigest", e); + } + md5.reset(); + md5.update(password.getBytes()); + byte[] digest = md5.digest(); + BigInteger bigInt = new BigInteger(1, digest); + String hashtext = bigInt.toString(16); + + vmData.add(new String[]{"password", "vm-password-md5checksum", hashtext}); + } + + vmData.add(new String[]{"password", "vm-password", password}); + } + + return vmData; + } } diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index 1661390baab..3364655b2fe 100644 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -3501,6 +3501,31 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir UserVmVO vm = _vmDao.findById(profile.getId()); Map details = _vmDetailsDao.listDetailsKeyPairs(vm.getId()); vm.setDetails(details); + + + // add userdata info into vm profile + Nic defaultNic = _networkModel.getDefaultNic(vm.getId()); + if(defaultNic != null) { + Network network = _networkModel.getNetwork(defaultNic.getNetworkId()); + if (_networkModel.isSharedNetworkWithoutServices(network.getId())) { + final String serviceOffering = _serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()).getDisplayText(); + final String zoneName = _dcDao.findById(vm.getDataCenterId()).getName(); + boolean isWindows = _guestOSCategoryDao.findById(_guestOSDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); + + List vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, zoneName, vm.getInstanceName(), vm.getId(), + (String) profile.getParameter(VirtualMachineProfile.Param.VmSshPubKey), (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows); + String vmName = vm.getInstanceName(); + String configDriveIsoRootFolder = "/tmp"; + String isoFile = configDriveIsoRootFolder + "/" + vmName + "/configDrive/" + vmName + ".iso"; + profile.setVmData(vmData); + profile.setConfigDriveLabel(VmConfigDriveLabel.value()); + profile.setConfigDriveIsoRootFolder(configDriveIsoRootFolder); + profile.setConfigDriveIsoFile(isoFile); + } + } + + + _templateMgr.prepareIsoForVmProfile(profile); return true; } @@ -5268,7 +5293,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {EnableDynamicallyScaleVm, VmIpFetchWaitInterval, VmIpFetchTrialMax, VmIpFetchThreadPoolMax}; + return new ConfigKey[] {EnableDynamicallyScaleVm, VmIpFetchWaitInterval, VmIpFetchTrialMax, VmIpFetchThreadPoolMax, VmConfigDriveLabel}; } @Override