diff --git a/api/src/com/cloud/network/NetworkModel.java b/api/src/com/cloud/network/NetworkModel.java index 35a6a63a9929..943efac5ba2b 100644 --- a/api/src/com/cloud/network/NetworkModel.java +++ b/api/src/com/cloud/network/NetworkModel.java @@ -73,7 +73,7 @@ public interface NetworkModel { AVAILABILITY_ZONE_FILE, "availability_zone", LOCAL_HOSTNAME_FILE, "hostname", VM_ID_FILE, "uuid", - INSTANCE_ID_FILE, "name" + PUBLIC_HOSTNAME_FILE, "name" ); static final ConfigKey MACIdentifier = new ConfigKey("Advanced",Integer.class, "mac.identifier", "0", diff --git a/config-drive/pom.xml b/config-drive/pom.xml new file mode 100644 index 000000000000..ea8c5b6710a8 --- /dev/null +++ b/config-drive/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + cloud-config-drive + Apache CloudStack Config-Drive Component + + org.apache.cloudstack + cloudstack + 4.11.1.0-SNAPSHOT + ../pom.xml + + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-core + ${project.version} + + + log4j + log4j + + + + + src/main/java + src/test/java + target/classes + target/test-classes + + + src/main/resources + + + + + src/test/resources + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + diff --git a/config-drive/src/main/java/org/apache/cloudstack/storage/ConfigDriveFactory.java b/config-drive/src/main/java/org/apache/cloudstack/storage/ConfigDriveFactory.java new file mode 100644 index 000000000000..0d8cfa3bf2a1 --- /dev/null +++ b/config-drive/src/main/java/org/apache/cloudstack/storage/ConfigDriveFactory.java @@ -0,0 +1,425 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.storage; + + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.HandleConfigDriveIsoCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.NfsTO; +import com.cloud.network.NetworkModel; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.io.Files; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import javax.naming.ConfigurationException; + +import static com.cloud.network.NetworkModel.CONFIGDATA_CONTENT; +import static com.cloud.network.NetworkModel.CONFIGDATA_DIR; +import static com.cloud.network.NetworkModel.CONFIGDATA_FILE; + +import static com.cloud.network.NetworkModel.METATDATA_DIR; +import static com.cloud.network.NetworkModel.PASSWORD_DIR; +import static com.cloud.network.NetworkModel.PASSWORD_FILE; +import static com.cloud.network.NetworkModel.PUBLIC_KEYS_FILE; +import static com.cloud.network.NetworkModel.USERDATA_DIR; +import static com.cloud.network.NetworkModel.USERDATA_FILE; + +public class ConfigDriveFactory { + public static final Logger s_logger = Logger.getLogger(ConfigDriveFactory.class); + + private static final String CLOUD_STACK_CONFIG_DRIVE_NAME = "/cloudstack/"; + private static final String OPEN_STACK_CONFIG_DRIVE_NAME = "/openstack/latest/"; + public static final String FAILED_TO_CREATE_FOLDER = "Failed to create folder "; + public static final String FAILED_TO_CREATE_FILE = "Failed to create file "; + public static final String EMPTY_SET = "{}"; + private final Integer nfsVersion; + protected boolean inSystemVM = false; + + private StorageAttacher attacher; + + private static final Map updatableConfigData = Maps.newHashMap(); + static { + + updatableConfigData.put(PUBLIC_KEYS_FILE, METATDATA_DIR); + updatableConfigData.put(USERDATA_FILE, USERDATA_DIR); + updatableConfigData.put(PASSWORD_FILE, PASSWORD_DIR); + } + + + private int timeout; + + public ConfigDriveFactory(Integer pNfsVersion, StorageAttacher pAttacher) { + nfsVersion = pNfsVersion; + attacher = pAttacher; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + private String linkUserData(String tempDirName) { + if(s_logger.isDebugEnabled()) { + s_logger.debug("Hard link the user_data.txt file with the user_data file in the OpenStack directory in " + tempDirName); + } + String userDataFilePath = tempDirName + CLOUD_STACK_CONFIG_DRIVE_NAME + "userdata/user_data.txt"; + if ((new File(userDataFilePath).exists())) { + Script hardLink = new Script(!inSystemVM, "ln", timeout, s_logger); + hardLink.add(userDataFilePath); + hardLink.add(tempDirName + OPEN_STACK_CONFIG_DRIVE_NAME + "user_data"); + if(s_logger.isDebugEnabled()) { + s_logger.debug("execute command: " + hardLink.toString()); + } + return hardLink.execute(); + } + return null; + } + + public Answer executeRequest(HandleConfigDriveIsoCommand cmd) { + + if(s_logger.isDebugEnabled()) { + s_logger.debug(String.format("VMdata %s, attach = %s", cmd.getVmData(), cmd.isCreate())); + } + if (cmd.isCreate()) { + if(cmd.getVmData() == null) return new Answer(cmd, false, "No Vmdata available"); + String nfsMountPoint = attacher.getRootDir(cmd.getDestStore().getUrl(), nfsVersion); + File isoFile = new File(nfsMountPoint, cmd.getIsoFile()); + if(isoFile.exists()) { + if (!cmd.isUpdate()) { + return new Answer(cmd, true, "ISO already available"); + } else { + // Find out if we have to recover the password/ssh-key from the already available ISO. + try { + List recoveredVmData = recoverVmData(isoFile); + for (String[] vmDataEntry : cmd.getVmData()) { + if (updatableConfigData.containsKey(vmDataEntry[CONFIGDATA_FILE]) + && updatableConfigData.get(vmDataEntry[CONFIGDATA_FILE]).equals(vmDataEntry[CONFIGDATA_DIR])) { + updateVmData(recoveredVmData, vmDataEntry); + } + } + cmd.setVmData(recoveredVmData); + } catch (IOException e) { + return new Answer(cmd, e); + } + } + } + return createConfigDriveIsoForVM(cmd); + } else { + DataStoreTO dstore = cmd.getDestStore(); + if (dstore instanceof NfsTO) { + NfsTO nfs = (NfsTO) dstore; + String relativeTemplatePath = new File(cmd.getIsoFile()).getParent(); + String nfsMountPoint = attacher.getRootDir(nfs.getUrl(), nfsVersion); + File tmpltPath = new File(nfsMountPoint, relativeTemplatePath); + try { + FileUtils.deleteDirectory(tmpltPath); + } catch (IOException e) { + return new Answer(cmd, e); + } + return new Answer(cmd); + } else { + return new Answer(cmd, false, "Not implemented yet"); + } + } + } + + public Answer createConfigDriveIsoForVM(HandleConfigDriveIsoCommand cmd) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("handling configdrive: " + cmd.getConfigDriveLabel()); + } + //create folder for the VM + if (cmd.getVmData() != null) { + + Path tempDir = null; + String tempDirName = null; + try { + tempDir = java.nio.file.Files.createTempDirectory("ConfigDrive"); + tempDirName = tempDir.toString(); + + //create OpenStack files + //create folder with empty files + File openStackFolder = new File(tempDirName + OPEN_STACK_CONFIG_DRIVE_NAME); + if (openStackFolder.exists() || openStackFolder.mkdirs()) { + File vendorDataFile = new File(openStackFolder,"vendor_data.json"); + WriteFileProcedure writeFileProcedure = new WriteFileProcedure(cmd, vendorDataFile, EMPTY_SET); + if (writeFileProcedure.invoke()) + return new Answer(cmd, writeFileProcedure.getEx()); + File networkDataFile = new File(openStackFolder, "network_data.json"); + writeFileProcedure = new WriteFileProcedure(cmd, networkDataFile, EMPTY_SET); + if (writeFileProcedure.invoke()) + return new Answer(cmd, writeFileProcedure.getEx()); + } else { + s_logger.error(FAILED_TO_CREATE_FOLDER + openStackFolder); + return new Answer(cmd, false, FAILED_TO_CREATE_FOLDER + openStackFolder); + } + + JsonObject metaData = new JsonObject(); + for (String[] item : cmd.getVmData()) { + String dataType = item[CONFIGDATA_DIR]; + String fileName = item[CONFIGDATA_FILE]; + String content = item[CONFIGDATA_CONTENT]; + s_logger.debug(String.format("[createConfigDriveIsoForVM] dataType=%s, filename=%s, content=%s", + dataType, fileName, (fileName.equals(PASSWORD_FILE)?"********":content))); + + // create file with content in folder + if (dataType != null && !dataType.isEmpty()) { + //create folder + File typeFolder = new File(tempDirName + CLOUD_STACK_CONFIG_DRIVE_NAME + dataType); + if (typeFolder.exists() || typeFolder.mkdirs()) { + if (StringUtils.isNotEmpty(content)) { + File file = new File(typeFolder, fileName + ".txt"); + WriteFileProcedure writeFileProcedure = new WriteFileProcedure(cmd, file, content); + if (writeFileProcedure.invoke()) + return new Answer(cmd, writeFileProcedure.getEx()); + } + } else { + s_logger.error(FAILED_TO_CREATE_FOLDER + typeFolder); + return new Answer(cmd, false, FAILED_TO_CREATE_FOLDER + typeFolder); + } + + //now write the file to the OpenStack directory + metaData = constructOpenStackMetaData(metaData, dataType, fileName, content); + } + } + + File metaDataFile = new File(openStackFolder, "meta_data.json"); + WriteFileProcedure writeFileProcedure = new WriteFileProcedure(cmd, metaDataFile, metaData.toString()); + if(writeFileProcedure.invoke()) + return new Answer(cmd,writeFileProcedure.getEx()); + + String linkResult = linkUserData(tempDirName); + if (linkResult != null) { + String errMsg = "Unable to create user_data link due to " + linkResult; + s_logger.warn(errMsg); + return new Answer(cmd, false, errMsg); + } + + File tmpIsoStore = new File(tempDirName, new File(cmd.getIsoFile()).getName()); + Script command = new Script(!inSystemVM, "/usr/bin/genisoimage", timeout, s_logger); + command.add("-o", tmpIsoStore.getAbsolutePath()); + command.add("-ldots"); + command.add("-allow-lowercase"); + command.add("-allow-multidot"); + command.add("-cache-inodes"); // Enable caching inode and device numbers to find hard links to files. + command.add("-l"); + command.add("-quiet"); + command.add("-J"); + command.add("-r"); + command.add("-V", cmd.getConfigDriveLabel()); + command.add(tempDirName); + s_logger.debug("execute command: " + command.toString()); + String result = command.execute(); + if (result != null) { + String errMsg = "Unable to create iso file: " + cmd.getIsoFile() + " due to " + result; + s_logger.warn(errMsg); + return new Answer(cmd, false, errMsg); + } + copyLocalToNfs(tmpIsoStore, new File(cmd.getIsoFile()), cmd.getDestStore()); + + } catch (IOException e) { + return new Answer(cmd, e); + } catch (ConfigurationException e) { + s_logger.warn("SecondStorageException ", e); + return new Answer(cmd, e); + } finally { + try { + FileUtils.deleteDirectory(tempDir.toFile()); + } catch (IOException ioe) { + s_logger.warn("Failed to delete ConfigDrive temporary directory: " + tempDirName, ioe); + } + } + } + return new Answer(cmd); + } + + private List recoverVmData(File isoFile) throws IOException { + String tempDirName = null; + List recoveredVmData = Lists.newArrayList(); + boolean mounted = false; + try { + Path tempDir = java.nio.file.Files.createTempDirectory("ConfigDrive"); + tempDirName = tempDir.toString(); + + // Unpack the current config drive file + Script command = new Script(!inSystemVM, "mount", timeout, s_logger); + command.add("-o", "loop"); + command.add(isoFile.getAbsolutePath()); + command.add(tempDirName); + String result = command.execute(); + + if (result != null) { + String errMsg = "Unable to mount " + isoFile.getAbsolutePath() + " at " + tempDirName + " due to " + result; + s_logger.error(errMsg); + throw new IOException(errMsg); + } + mounted = true; + + + // Scan directory structure + for (File configDirectory: (new File(tempDirName, "cloudstack")).listFiles()){ + for (File configFile: configDirectory.listFiles()) { + recoveredVmData.add(new String[]{configDirectory.getName(), + Files.getNameWithoutExtension(configFile.getName()), + Files.readFirstLine(configFile, Charset.defaultCharset())}); + } + } + + } finally { + if (mounted) { + Script command = new Script(!inSystemVM, "umount", timeout, s_logger); + command.add(tempDirName); + String result = command.execute(); + if (result != null) { + s_logger.warn("Unable to umount " + tempDirName + " due to " + result); + } + } + try { + FileUtils.deleteDirectory(new File(tempDirName)); + } catch (IOException ioe) { + s_logger.warn("Failed to delete ConfigDrive temporary directory: " + tempDirName, ioe); + } + } + return recoveredVmData; + } + JsonObject constructOpenStackMetaData(JsonObject metaData, String dataType, String fileName, String content) { + if(s_logger.isDebugEnabled()) { + s_logger.debug("adding " + content + " to " + fileName); + } + if (dataType.equals(NetworkModel.METATDATA_DIR) && StringUtils.isNotEmpty(content)) { + //keys are a special case in OpenStack format + if (NetworkModel.PUBLIC_KEYS_FILE.equals(fileName)) { + String[] keyArray = content.replace("\\n", "").split(" "); + String keyName = "key"; + if (keyArray.length > 3 && StringUtils.isNotEmpty(keyArray[2])){ + keyName = keyArray[2]; + } + + JsonObject keyLegacy = new JsonObject(); + keyLegacy.addProperty("type", "ssh"); + keyLegacy.addProperty("data", content.replace("\\n", "")); + keyLegacy.addProperty("name", keyName); + metaData.add("keys", arrayOf(keyLegacy)); + + JsonObject key = new JsonObject(); + key.addProperty(keyName, content); + metaData.add("public_keys", key); + } else if (NetworkModel.openStackFileMapping.get(fileName) != null) { + metaData.addProperty(NetworkModel.openStackFileMapping.get(fileName), content); + } + } + return metaData; + } + private static JsonArray arrayOf(JsonElement... elements) { + JsonArray array = new JsonArray(); + for (JsonElement element : elements) { + array.add(element); + } + return array; + } + + private void updateVmData(List recoveredVmData, String[] vmDataEntry) { + for (String[] recoveredEntry : recoveredVmData) { + if (recoveredEntry[CONFIGDATA_DIR].equals(vmDataEntry[CONFIGDATA_DIR]) + && recoveredEntry[CONFIGDATA_FILE].equals(vmDataEntry[CONFIGDATA_FILE])) { + recoveredEntry[CONFIGDATA_CONTENT] = vmDataEntry[CONFIGDATA_CONTENT]; + return; + } + } + recoveredVmData.add(vmDataEntry); + } + + protected void copyLocalToNfs(File localFile, File isoFile, DataStoreTO destData) throws ConfigurationException, IOException { + if(s_logger.isDebugEnabled()) { + s_logger.debug("copyfest: from " + localFile + " to " + isoFile + " on " + destData.getUrl()); + } + String scriptsDir = "scripts/storage/secondary"; + String createVolScr = Script.findScript(scriptsDir, "createvolume.sh"); + if (createVolScr == null) { + throw new ConfigurationException("Unable to find createvolume.sh"); + } + s_logger.info("createvolume.sh found in " + createVolScr); + + int installTimeoutPerGig = 180 * 60 * 1000; + int imgSizeGigs = (int) Math.ceil(localFile.length() * 1.0d / (1024 * 1024 * 1024)); + imgSizeGigs++; // add one just in case + long timeout = imgSizeGigs * installTimeoutPerGig; + + Script scr = new Script(createVolScr, timeout, s_logger); + scr.add("-s", Integer.toString(imgSizeGigs)); + scr.add("-n", isoFile.getName()); + scr.add("-t", attacher.getRootDir(destData.getUrl(), nfsVersion) + "/" + isoFile.getParent()); + scr.add("-f", localFile.getAbsolutePath()); + scr.add("-d", "configDrive"); + if(s_logger.isDebugEnabled()) { + s_logger.debug("executing " + scr.toString()); + } + String result; + result = scr.execute(); + + if (result != null) { + // script execution failure + throw new CloudRuntimeException("Failed to run script " + createVolScr); + } + } + + private class WriteFileProcedure { + private HandleConfigDriveIsoCommand cmd; + private File vendorDataFile; + private String content; + private IOException ex; + + public WriteFileProcedure(HandleConfigDriveIsoCommand cmd, File vendorDataFile, String content) { + this.cmd = cmd; + this.vendorDataFile = vendorDataFile; + this.content = content; + } + + public IOException getEx() { + return ex; + } + + public boolean invoke() { + try (FileWriter fw = new FileWriter(vendorDataFile); BufferedWriter bw = new BufferedWriter(fw)) { + bw.write(content); + } catch (IOException ex) {ConfigDriveFactory.WriteFileProcedure.this.ex = ex; + s_logger.error(FAILED_TO_CREATE_FILE, ex); + return true; + } + return false; + } + } +} diff --git a/config-drive/src/main/java/org/apache/cloudstack/storage/StorageAttacher.java b/config-drive/src/main/java/org/apache/cloudstack/storage/StorageAttacher.java new file mode 100644 index 000000000000..8809a52087e1 --- /dev/null +++ b/config-drive/src/main/java/org/apache/cloudstack/storage/StorageAttacher.java @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.storage; + +import java.net.URI; + +public interface StorageAttacher { + String getRootDir(String url, Integer nfsVersion); + + void umount(String localRootPath, URI uri); + + void mount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion); +} diff --git a/config-drive/src/test/java/org/apache/cloudstack/storage/ConfigDriveFactoryTest.java b/config-drive/src/test/java/org/apache/cloudstack/storage/ConfigDriveFactoryTest.java new file mode 100644 index 000000000000..d238b654175d --- /dev/null +++ b/config-drive/src/test/java/org/apache/cloudstack/storage/ConfigDriveFactoryTest.java @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.storage; + +import com.cloud.agent.api.HandleConfigDriveIsoCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.storage.DataStoreRole; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class ConfigDriveFactoryTest { + private final static String CONFIGDRIVEFILENAME = "configdrive.iso"; + private final static String CONFIGDRIVEDIR = "ConfigDrive"; + + @Mock + StorageAttacher storageAttacher; + + @Test + public void executeRequest() { + List vmData = null; + String label = null; + DataStoreTO destStore = new DataStoreTO() { + @Override public DataStoreRole getRole() { + return DataStoreRole.Primary; + } + + @Override public String getUuid() { + return "0123456789abcdef"; + } + + @Override public String getUrl() { + return ""; + } + + @Override public String getPathSeparator() { + return "/"; + } + }; + String isoFile = "/" + CONFIGDRIVEDIR + "/" + "bla" + "/" + CONFIGDRIVEFILENAME; + boolean create = false; + boolean update = false; + HandleConfigDriveIsoCommand cmd = new HandleConfigDriveIsoCommand(null, + null, destStore, isoFile, false, false); + ConfigDriveFactory configDriveFactory = new ConfigDriveFactory(1,storageAttacher); + configDriveFactory.executeRequest(cmd); + } +} \ No newline at end of file diff --git a/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java b/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java index d6d87d48c054..8f61f579b2ff 100644 --- a/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java +++ b/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java @@ -64,6 +64,10 @@ public String getConfigDriveLabel() { return configDriveLabel; } + /** + * + * @return a PrimaryDataStore when executed in a HostAgent else a SecStore + */ public DataStoreTO getDestStore() { return destStore; } diff --git a/engine/storage/pom.xml b/engine/storage/pom.xml index af6382c535d1..7a321825bf7e 100644 --- a/engine/storage/pom.xml +++ b/engine/storage/pom.xml @@ -30,13 +30,6 @@ cloud-core ${project.version} - org.apache.cloudstack cloud-engine-components-api diff --git a/packaging/centos63/cloud.spec b/packaging/centos63/cloud.spec index 87d2c1ec5780..9655ba72d72c 100644 --- a/packaging/centos63/cloud.spec +++ b/packaging/centos63/cloud.spec @@ -130,6 +130,7 @@ Requires: perl Requires: libvirt-python Requires: qemu-img Requires: qemu-kvm +Requires: genisoimage Provides: cloud-agent Obsoletes: cloud-agent < 4.1.0 Obsoletes: cloud-agent-libs < 4.1.0 diff --git a/packaging/centos7/cloud.spec b/packaging/centos7/cloud.spec index 525421c0108d..0a8292e1012b 100644 --- a/packaging/centos7/cloud.spec +++ b/packaging/centos7/cloud.spec @@ -111,6 +111,7 @@ Requires: perl Requires: libvirt-python Requires: qemu-img Requires: qemu-kvm +Requires: genisoimage Provides: cloud-agent Group: System Environment/Libraries %description agent diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index d90e79adfc9b..0433f4b175ee 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -39,6 +39,11 @@ cloud-plugin-network-ovs ${project.version} + + org.apache.cloudstack + cloud-config-drive + ${project.version} + com.ceph rados diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index fc5e5395b87f..5feb6db4115e 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -26,6 +26,7 @@ import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -47,6 +48,8 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import org.apache.cloudstack.storage.ConfigDriveFactory; +import org.apache.cloudstack.storage.StorageAttacher; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.hypervisor.HypervisorUtils; @@ -192,7 +195,7 @@ * private mac addresses for domrs | mac address | start + 126 || || * pool | the parent of the storage pool hierarchy * } **/ -public class LibvirtComputingResource extends ServerResourceBase implements ServerResource, VirtualRouterDeployer { +public class LibvirtComputingResource extends ServerResourceBase implements ServerResource, VirtualRouterDeployer, StorageAttacher { private static final Logger s_logger = Logger.getLogger(LibvirtComputingResource.class); private String _modifyVlanPath; @@ -314,6 +317,12 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv private final LibvirtUtilitiesHelper libvirtUtilitiesHelper = new LibvirtUtilitiesHelper(); + ConfigDriveFactory configDriveFactory; + + public ConfigDriveFactory getConfigDriveFactory() { + return configDriveFactory; + } + @Override public ExecutionResult executeInVR(final String routerIp, final String script, final String args) { return executeInVR(routerIp, script, args, _timeout); @@ -481,6 +490,136 @@ public StorageSubsystemCommandHandler getStorageHandler() { return storageHandler; } + @Override + synchronized public String getRootDir(String url, Integer nfsVersion) { + try { + URI uri = new URI(url); + String dir = mountUri(uri, nfsVersion); + return _mountPoint + "/" + dir; + } catch (Exception e) { + String msg = "GetRootDir for " + url + " failed due to " + e.toString(); + s_logger.error(msg, e); + throw new CloudRuntimeException(msg); + } + } + + protected String mountUri(URI uri, Integer nfsVersion) throws UnknownHostException { + String uriHostIp = getUriHostIp(uri); + String nfsPath = uriHostIp + ":" + uri.getPath(); + + // Single means of calculating mount directory regardless of scheme + String dir = UUID.nameUUIDFromBytes(nfsPath.getBytes(com.cloud.utils.StringUtils.getPreferredCharset())).toString(); + String localRootPath = _mountPoint + "/" + dir; + + // remote device syntax varies by scheme. + String remoteDevice = nfsPath; + s_logger.debug("Mounting device with nfs-style path of " + remoteDevice); + + mount(localRootPath, remoteDevice, uri, nfsVersion); + return dir; + } + + protected String getUriHostIp(URI uri) throws UnknownHostException { + String nfsHost = uri.getHost(); + InetAddress nfsHostAddr = InetAddress.getByName(nfsHost); + String nfsHostIp = nfsHostAddr.getHostAddress(); + s_logger.info("Determined host " + nfsHost + " corresponds to IP " + nfsHostIp); + return nfsHostIp; + } + + @Override public void umount(String localRootPath, URI uri) { + ensureLocalRootPathExists(localRootPath, uri); + + if (!mountExists(localRootPath, uri)) { + return; + } + + Script command = new Script("mount", _timeout, s_logger); + command.add(localRootPath); + String result = command.execute(); + if (result != null) { + // Fedora Core 12 errors out with any -o option executed from java + String errMsg = "Unable to umount " + localRootPath + " due to " + result; + s_logger.error(errMsg); + File file = new File(localRootPath); + if (file.exists()) { + file.delete(); + } + throw new CloudRuntimeException(errMsg); + } + s_logger.debug("Successfully umounted " + localRootPath); + } + + protected boolean mountExists(String localRootPath, URI uri) { + Script script = null; + script = new Script( "mount", _timeout, s_logger); + + List res = new ArrayList(); + AllLinesParser parser = new AllLinesParser(); + script.execute(parser); + if(parser.getLines().contains(localRootPath)) { + s_logger.debug("Some device already mounted at " + localRootPath + ", no need to mount " + uri.toString()); + return true; + } + return false; + } + + protected void ensureLocalRootPathExists(String localRootPath, URI uri) { + s_logger.debug("making available " + localRootPath + " on " + uri.toString()); + File file = new File(localRootPath); + s_logger.debug("local folder for mount will be " + file.getPath()); + if (!file.exists()) { + s_logger.debug("create mount point: " + file.getPath()); + _storage.mkdir(file.getPath()); + + // Need to check after mkdir to allow O/S to complete operation + if (!file.exists()) { + String errMsg = "Unable to create local folder for: " + localRootPath + " in order to mount " + uri.toString(); + s_logger.error(errMsg); + throw new CloudRuntimeException(errMsg); + } + } + } + + @Override + public void mount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion) { + s_logger.debug("mount " + uri.toString() + " on " + localRootPath + ((nfsVersion != null) ? " nfsVersion=" + nfsVersion : "")); + ensureLocalRootPathExists(localRootPath, uri); + + if (mountExists(localRootPath, uri)) { + return; + } + + attemptMount(localRootPath, remoteDevice, uri, nfsVersion); + } + + protected void attemptMount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion) { + String result; + s_logger.debug("Make cmdline call to mount " + remoteDevice + " at " + localRootPath + " based on uri " + uri + ((nfsVersion != null) ? " nfsVersion=" + nfsVersion : "")); + Script command = new Script("mount", _timeout, s_logger); + + String scheme = uri.getScheme().toLowerCase(); + if("networkfilesystem".equals(scheme)) { + scheme = "nfs"; + } + command.add("-t", scheme); + + command.add(remoteDevice); + command.add(localRootPath); + result = command.execute(); + if (result != null) { + // Fedora Core 12 errors out with any -o option executed from java + String errMsg = "Unable to mount " + remoteDevice + " at " + localRootPath + " due to " + result; + s_logger.error(errMsg); + File file = new File(localRootPath); + if (file.exists()) { + file.delete(); + } + throw new CloudRuntimeException(errMsg); + } + s_logger.debug("Successfully mounted " + remoteDevice + " at " + localRootPath); + } + private static final class KeyValueInterpreter extends OutputInterpreter { private final Map map = new HashMap(); @@ -987,18 +1126,6 @@ public boolean configure(final String name, final Map params) th configureVifDrivers(params); - /* - switch (_bridgeType) { - case OPENVSWITCH: - getOvsPifs(); - break; - case NATIVE: - default: - getPifs(); - break; - } - */ - if (_pifs.get("private") == null) { s_logger.debug("Failed to get private nic name"); throw new ConfigurationException("Failed to get private nic name"); @@ -1061,6 +1188,8 @@ public boolean configure(final String name, final Map params) th storageProcessor.configure(name, params); storageHandler = new StorageSubsystemCommandHandlerBase(storageProcessor); + configDriveFactory = new ConfigDriveFactory(3, this); + return true; } diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtHandleConfigDriveCommandWrapper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtHandleConfigDriveCommandWrapper.java new file mode 100644 index 000000000000..ce7d7c7ec004 --- /dev/null +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtHandleConfigDriveCommandWrapper.java @@ -0,0 +1,34 @@ +// +//Licensed to the Apache Software Foundation (ASF) under one +//or more contributor license agreements. See the NOTICE file +//distributed with this work for additional information +//regarding copyright ownership. The ASF licenses this file +//to you 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.HandleConfigDriveIsoCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; + +@ResourceWrapper(handles = HandleConfigDriveIsoCommand.class) +public class LibvirtHandleConfigDriveCommandWrapper extends CommandWrapper +{ + @Override public Answer execute(HandleConfigDriveIsoCommand command, LibvirtComputingResource serverResource) { + return serverResource.getConfigDriveFactory().executeRequest(command); + } +} diff --git a/pom.xml b/pom.xml index 6ab0afcd63a9..6b389324364e 100644 --- a/pom.xml +++ b/pom.xml @@ -199,6 +199,7 @@ tools/checkstyle api agent + config-drive core server usage diff --git a/server/pom.xml b/server/pom.xml index 534b1a34c70d..76f06d445b62 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -32,6 +32,11 @@ cloud-core ${project.version} + + org.apache.cloudstack + cloud-config-drive + ${project.version} + org.apache.cloudstack cloud-framework-cluster diff --git a/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java b/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java index cc1df935388a..482b57061d17 100644 --- a/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java +++ b/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java @@ -22,6 +22,15 @@ import java.util.Set; import javax.inject.Inject; +import com.cloud.exception.CloudException; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.DataStoreRole; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VolumeDao; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.log4j.Logger; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; @@ -78,7 +87,7 @@ import com.cloud.vm.dao.UserVmDetailsDao; public class ConfigDriveNetworkElement extends AdapterBase implements NetworkElement, UserDataServiceProvider, - StateListener, NetworkMigrationResponder { + StateListener, NetworkMigrationResponder, Configurable { private static final Logger s_logger = Logger.getLogger(ConfigDriveNetworkElement.class); private static final Map> capabilities = setCapabilities(); @@ -117,6 +126,11 @@ public class ConfigDriveNetworkElement extends AdapterBase implements NetworkEle EndPointSelector _ep; @Inject VolumeOrchestrationService _volumeMgr; + @Inject + VolumeDao volumeDao; + + @Inject + PrimaryDataStoreDao dataStoreDao; private final static String CONFIGDRIVEFILENAME = "configdrive.iso"; private final static String CONFIGDRIVEDIR = "ConfigDrive"; @@ -141,6 +155,9 @@ public boolean implement(Network network, NetworkOffering offering, DeployDestin @Override public boolean prepare(Network network, NicProfile nic, VirtualMachineProfile vmProfile, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { + if (s_logger.isDebugEnabled()) { + s_logger.debug("\"preparing\" config drive for vm " + vmProfile.getInstanceName()); + } return true; } @@ -149,22 +166,70 @@ public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm if (!nic.isDefaultNic()) { return true; } - // Remove form secondary storage - DataStore secondaryStore = _dataStoreMgr.getImageStore(network.getDataCenterId()); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("releasing network for configdrive of vm " + vm.getInstanceName()); + } + + DataStore dataStore = null; + try { + dataStore = getDataStore(network, vm, null); + } catch (CloudException e) { + s_logger.info("no primary storage found, nothing to release for vm "+ vm.getInstanceName()); + return true; + } String isoFile = "/" + CONFIGDRIVEDIR + "/" + vm.getInstanceName()+ "/" + CONFIGDRIVEFILENAME; HandleConfigDriveIsoCommand deleteCommand = new HandleConfigDriveIsoCommand(vm.getVmData(), - vm.getConfigDriveLabel(), secondaryStore.getTO(), isoFile, false, false); - // Delete the ISO on the secondary store - EndPoint endpoint = _ep.select(secondaryStore); + vm.getConfigDriveLabel(), dataStore.getTO(), isoFile, false, false); + s_logger.info("Delete the ISO on the secondary store; " + dataStore); + EndPoint endpoint = _ep.select(dataStore); if (endpoint == null) { - s_logger.error(String.format("Secondary store: %s not available", secondaryStore.getName())); + s_logger.error(String.format("ConfigDrive store: %s not available", dataStore.getName())); return false; } Answer answer = endpoint.sendMessage(deleteCommand); return answer.getResult(); } + private DataStore getDataStore(Network network, VirtualMachineProfile vm, Long hostId) throws CloudException { + DataStore dataStore = null; + if(UsePrimaryStorage.value() && Hypervisor.HypervisorType.KVM.equals(vm.getHypervisorType())) { + dataStore = getPrimaryDatastoreForVM(vm); + } else { + dataStore = _dataStoreMgr.getImageStore(network.getDataCenterId()); + } + return dataStore; + } + + private DataStore getPrimaryDatastoreForVM(VirtualMachineProfile vm) throws CloudException { + DataStore dataStore = null; + List volumes = volumeDao.findUsableVolumesForInstance(vm.getId()); + for(VolumeVO volume: volumes) { + Long poolId = volume.getPoolId(); + if(poolId != null) { + dataStore = _dataStoreMgr.getPrimaryDataStore(poolId); + if (DataStoreRole.Primary.equals(dataStore.getRole())) { + return dataStore; + } + } + } + List dataStores = dataStoreDao.listAll(); + if(!dataStores.isEmpty()) { + if(s_logger.isDebugEnabled()) { + s_logger.debug("no data stores found for volumes belonging to vm " + vm.getInstanceName()); + } + dataStore = _dataStoreMgr.getPrimaryDataStore(dataStores.get(0).getId()); + if(s_logger.isDebugEnabled()) { + s_logger.debug("using generic store " + dataStore.getName() + " to create ConfigDrive for vm " + vm.getInstanceName()); + } + } + if(dataStore == null) { + throw new CloudException("No data store found to put configdrive for vm " + vm.getInstanceName()); + } + return dataStore; + } + @Override public boolean shutdown(Network network, ReservationContext context, boolean cleanup) throws ConcurrentOperationException, ResourceUnavailableException { return true; // assume that the agent will remove userdata etc @@ -215,6 +280,9 @@ private String getSshKey(VirtualMachineProfile profile) { public boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { String sshPublicKey = getSshKey(profile); + if (s_logger.isDebugEnabled()) { + s_logger.debug("adding password and userdata to configdrive for vm " + profile.getInstanceName()); + } return (canHandle(network.getTrafficType()) && updateConfigDrive(profile, sshPublicKey, nic)) && updateConfigDriveIso(network, profile, dest.getHost(), false); @@ -223,20 +291,38 @@ && updateConfigDrive(profile, sshPublicKey, nic)) @Override public boolean savePassword(Network network, NicProfile nic, VirtualMachineProfile profile) throws ResourceUnavailableException { String sshPublicKey = getSshKey(profile); + if (s_logger.isDebugEnabled()) { + s_logger.debug("saving password to configdrive for vm " + profile.getInstanceName()); + } if (!(canHandle(network.getTrafficType()) && updateConfigDrive(profile, sshPublicKey, nic))) return false; + if (s_logger.isDebugEnabled()) { + s_logger.debug("update configdrive iso with password for vm " + profile.getInstanceName()); + } return updateConfigDriveIso(network, profile, true); } @Override public boolean saveSSHKey(Network network, NicProfile nic, VirtualMachineProfile vm, String sshPublicKey) throws ResourceUnavailableException { + if (s_logger.isDebugEnabled()) { + s_logger.debug("saving ssh key to configdrive for vm " + vm.getInstanceName()); + } if (!(canHandle(network.getTrafficType()) && updateConfigDrive(vm, sshPublicKey, nic))) return false; + if (s_logger.isDebugEnabled()) { + s_logger.debug("updating configdrive iso with ssh key for vm " + vm.getInstanceName()); + } return updateConfigDriveIso(network, vm, true); } @Override public boolean saveUserData(Network network, NicProfile nic, VirtualMachineProfile profile) throws ResourceUnavailableException { String sshPublicKey = getSshKey(profile); + if (s_logger.isDebugEnabled()) { + s_logger.debug("saving userdata to configdrive for vm " + profile.getInstanceName()); + } if (!(canHandle(network.getTrafficType()) && updateConfigDrive(profile, sshPublicKey, nic))) return false; + if (s_logger.isDebugEnabled()) { + s_logger.debug("updating configdrive iso with userdata for vm " + profile.getInstanceName()); + } return updateConfigDriveIso(network, profile, true); } @@ -247,12 +333,18 @@ public boolean verifyServicesCombination(Set services) { @Override public boolean preStateTransitionEvent(VirtualMachine.State oldState, VirtualMachine.Event event, VirtualMachine.State newState, VirtualMachine vo, boolean status, Object opaque) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("preparing to transistion state from " + oldState + " on event " + event + " for vm " + vo.getInstanceName()); + } return true; } @Override public boolean postStateTransitionEvent(StateMachine2.Transition transition, VirtualMachine vo, boolean status, Object opaque) { if (transition.getToState().equals(VirtualMachine.State.Expunging) && transition.getEvent().equals(VirtualMachine.Event.ExpungeOperation)) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("handling expunge state for configdrive of vm " + vo.getInstanceName()); + } Nic nic = _networkModel.getDefaultNic(vo.getId()); try { if (nic != null) { @@ -261,13 +353,13 @@ public boolean postStateTransitionEvent(StateMachine2.Transition UsePrimaryStorage = new ConfigKey(Boolean.class, "configdrive.use.primary", "Advanced", "false", + "Whether to use primary storage instead of secondary to create config drive ISOs", false, ConfigKey.Scope.Global, null); + + @Override public String getConfigComponentName() { + return ConfigDriveNetworkElement.class.getSimpleName(); + } + + @Override public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {UsePrimaryStorage}; + } } diff --git a/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java b/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java index 7d5041536f05..b64919ddffa9 100644 --- a/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java +++ b/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java @@ -166,6 +166,7 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException { when(virtualMachine.getDataCenterId()).thenReturn(DATACENTERID); when(virtualMachine.getInstanceName()).thenReturn(VMINSTANCENAME); when(virtualMachine.getUserData()).thenReturn(VMUSERDATA); + when(virtualMachine.getHostName()).thenReturn("vm-hostname"); when(deployDestination.getHost()).thenReturn(hostVO); when(hostVO.getId()).thenReturn(HOSTID); when(nic.isDefaultNic()).thenReturn(true); diff --git a/services/secondary-storage/server/pom.xml b/services/secondary-storage/server/pom.xml index 7c7cf9b68bff..c30b248a5b0c 100644 --- a/services/secondary-storage/server/pom.xml +++ b/services/secondary-storage/server/pom.xml @@ -40,11 +40,16 @@ commons-codec - - org.apache.cloudstack - cloud-agent - ${project.version} - + + org.apache.cloudstack + cloud-agent + ${project.version} + + + org.apache.cloudstack + cloud-config-drive + ${project.version} + org.apache.cloudstack cloud-server diff --git a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/LocalNfsSecondaryStorageResource.java b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/LocalNfsSecondaryStorageResource.java index c02fe8a610e4..b2d537831ed4 100644 --- a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/LocalNfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/LocalNfsSecondaryStorageResource.java @@ -53,20 +53,20 @@ public Answer executeRequest(Command cmd) { } @Override - synchronized public String getRootDir(String secUrl, Integer nfsVersion) { + synchronized public String getRootDir(String url, Integer nfsVersion) { try { - URI uri = new URI(secUrl); + URI uri = new URI(url); String dir = mountUri(uri, nfsVersion); return _parent + "/" + dir; } catch (Exception e) { - String msg = "GetRootDir for " + secUrl + " failed due to " + e.toString(); + String msg = "GetRootDir for " + url + " failed due to " + e.toString(); s_logger.error(msg, e); throw new CloudRuntimeException(msg); } } @Override - protected void mount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion) { + public void mount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion) { ensureLocalRootPathExists(localRootPath, uri); if (mountExists(localRootPath, uri)) { diff --git a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index da3ab32e2035..c32287ed12c3 100644 --- a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -16,15 +16,6 @@ // under the License. package org.apache.cloudstack.storage.resource; -import static com.cloud.network.NetworkModel.CONFIGDATA_CONTENT; -import static com.cloud.network.NetworkModel.CONFIGDATA_DIR; -import static com.cloud.network.NetworkModel.CONFIGDATA_FILE; -import static com.cloud.network.NetworkModel.METATDATA_DIR; -import static com.cloud.network.NetworkModel.PASSWORD_DIR; -import static com.cloud.network.NetworkModel.PASSWORD_FILE; -import static com.cloud.network.NetworkModel.PUBLIC_KEYS_FILE; -import static com.cloud.network.NetworkModel.USERDATA_DIR; -import static com.cloud.network.NetworkModel.USERDATA_FILE; import static com.cloud.utils.StringUtils.join; import static com.cloud.utils.storage.S3.S3Utils.putFile; import static java.lang.String.format; @@ -45,8 +36,6 @@ import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; -import java.nio.charset.Charset; -import java.nio.file.Path; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; @@ -70,7 +59,7 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; -import org.apache.commons.codec.binary.Base64; +import org.apache.cloudstack.storage.ConfigDriveFactory; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; @@ -87,16 +76,11 @@ import org.joda.time.format.ISODateTimeFormat; import com.amazonaws.services.s3.model.S3ObjectSummary; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import org.apache.cloudstack.framework.security.keystore.KeystoreManager; +import org.apache.cloudstack.storage.StorageAttacher; import org.apache.cloudstack.storage.command.CopyCmdAnswer; import org.apache.cloudstack.storage.command.CopyCommand; import org.apache.cloudstack.storage.command.DeleteCommand; @@ -164,7 +148,6 @@ import com.cloud.host.Host; import com.cloud.host.Host.Type; import com.cloud.hypervisor.Hypervisor.HypervisorType; -import com.cloud.network.NetworkModel; import com.cloud.resource.ServerResourceBase; import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; @@ -193,23 +176,13 @@ import com.cloud.utils.storage.S3.S3Utils; import com.cloud.vm.SecondaryStorageVm; -public class NfsSecondaryStorageResource extends ServerResourceBase implements SecondaryStorageResource { +public class NfsSecondaryStorageResource extends ServerResourceBase implements SecondaryStorageResource, StorageAttacher { public static final Logger s_logger = Logger.getLogger(NfsSecondaryStorageResource.class); private static final String TEMPLATE_ROOT_DIR = "template/tmpl"; private static final String VOLUME_ROOT_DIR = "volumes"; private static final String POST_UPLOAD_KEY_LOCATION = "/etc/cloudstack/agent/ms-psk"; - private static final String cloudStackConfigDriveName = "/cloudstack/"; - private static final String openStackConfigDriveName = "/openstack/latest/"; - - private static final Map updatableConfigData = Maps.newHashMap(); - static { - - updatableConfigData.put(PUBLIC_KEYS_FILE, METATDATA_DIR); - updatableConfigData.put(USERDATA_FILE, USERDATA_DIR); - updatableConfigData.put(PASSWORD_FILE, PASSWORD_DIR); - } int _timeout; @@ -253,6 +226,7 @@ public void setTimeout(int timeout) { private String _ssvmPSK = null; private long processTimeout; + ConfigDriveFactory configDriveFactory; public void setParentPath(String path) { _parent = path; } @@ -281,7 +255,7 @@ public static Integer retrieveNfsVersionFromParams(Map params) { try { nfsVersion = Integer.valueOf(nfsVersionParam); } catch (NumberFormatException e){ - s_logger.error("Couldn't cast " + nfsVersionParam + " to integer"); + s_logger.error("No NFS version known; Couldn't cast " + nfsVersionParam + " to integer"); return null; } } @@ -338,309 +312,13 @@ public Answer executeRequest(Command cmd) { } private Answer execute(HandleConfigDriveIsoCommand cmd) { - - if (cmd.isCreate()) { - s_logger.debug(String.format("VMdata %s, attach = %s", cmd.getVmData(), cmd.isCreate())); - if(cmd.getVmData() == null) return new Answer(cmd, false, "No Vmdata available"); - String nfsMountPoint = getRootDir(cmd.getDestStore().getUrl(), _nfsVersion); - File isoFile = new File(nfsMountPoint, cmd.getIsoFile()); - if(isoFile.exists()) { - if (!cmd.isUpdate()) { - return new Answer(cmd, true, "ISO already available"); - } else { - // Find out if we have to recover the password/ssh-key from the already available ISO. - try { - List recoveredVmData = recoverVmData(isoFile); - for (String[] vmDataEntry : cmd.getVmData()) { - if (updatableConfigData.containsKey(vmDataEntry[CONFIGDATA_FILE]) - && updatableConfigData.get(vmDataEntry[CONFIGDATA_FILE]).equals(vmDataEntry[CONFIGDATA_DIR])) { - updateVmData(recoveredVmData, vmDataEntry); - } - } - cmd.setVmData(recoveredVmData); - } catch (IOException e) { - return new Answer(cmd, e); - } - } - } - return createConfigDriveIsoForVM(cmd); - } else { - DataStoreTO dstore = cmd.getDestStore(); - if (dstore instanceof NfsTO) { - NfsTO nfs = (NfsTO) dstore; - String relativeTemplatePath = new File(cmd.getIsoFile()).getParent(); - String nfsMountPoint = getRootDir(nfs.getUrl(), _nfsVersion); - File tmpltPath = new File(nfsMountPoint, relativeTemplatePath); - try { - FileUtils.deleteDirectory(tmpltPath); - } catch (IOException e) { - return new Answer(cmd, e); - } - return new Answer(cmd); - } else { - return new Answer(cmd, false, "Not implemented yet"); - } - } - } - - private void updateVmData(List recoveredVmData, String[] vmDataEntry) { - for (String[] recoveredEntry : recoveredVmData) { - if (recoveredEntry[CONFIGDATA_DIR].equals(vmDataEntry[CONFIGDATA_DIR]) - && recoveredEntry[CONFIGDATA_FILE].equals(vmDataEntry[CONFIGDATA_FILE])) { - recoveredEntry[CONFIGDATA_CONTENT] = vmDataEntry[CONFIGDATA_CONTENT]; - return; - } - } - recoveredVmData.add(vmDataEntry); - } - - private List recoverVmData(File isoFile) throws IOException { - String tempDirName = null; - List recoveredVmData = Lists.newArrayList(); - boolean mounted = false; - try { - Path tempDir = java.nio.file.Files.createTempDirectory("ConfigDrive"); - tempDirName = tempDir.toString(); - - // Unpack the current config drive file - Script command = new Script(!_inSystemVM, "mount", _timeout, s_logger); - command.add("-o", "loop"); - command.add(isoFile.getAbsolutePath()); - command.add(tempDirName); - String result = command.execute(); - - if (result != null) { - String errMsg = "Unable to mount " + isoFile.getAbsolutePath() + " at " + tempDirName + " due to " + result; - s_logger.error(errMsg); - throw new IOException(errMsg); - } - mounted = true; - - - // Scan directory structure - for (File configDirectory: (new File(tempDirName, "cloudstack")).listFiles()){ - for (File configFile: configDirectory.listFiles()) { - recoveredVmData.add(new String[]{configDirectory.getName(), - Files.getNameWithoutExtension(configFile.getName()), - Files.readFirstLine(configFile, Charset.defaultCharset())}); - } - } - - } finally { - if (mounted) { - Script command = new Script(!_inSystemVM, "umount", _timeout, s_logger); - command.add(tempDirName); - String result = command.execute(); - if (result != null) { - s_logger.warn("Unable to umount " + tempDirName + " due to " + result); - } - } - try { - FileUtils.deleteDirectory(new File(tempDirName)); - } catch (IOException ioe) { - s_logger.warn("Failed to delete ConfigDrive temporary directory: " + tempDirName, ioe); - } - } - return recoveredVmData; - } - - public Answer createConfigDriveIsoForVM(HandleConfigDriveIsoCommand cmd) { - //create folder for the VM - if (cmd.getVmData() != null) { - - Path tempDir = null; - String tempDirName = null; - try { - tempDir = java.nio.file.Files.createTempDirectory("ConfigDrive"); - tempDirName = tempDir.toString(); - - //create OpenStack files - //create folder with empty files - File openStackFolder = new File(tempDirName + openStackConfigDriveName); - if (openStackFolder.exists() || openStackFolder.mkdirs()) { - File vendorDataFile = new File(openStackFolder,"vendor_data.json"); - try (FileWriter fw = new FileWriter(vendorDataFile); BufferedWriter bw = new BufferedWriter(fw)) { - bw.write("{}"); - } catch (IOException ex) { - s_logger.error("Failed to create file ", ex); - return new Answer(cmd, ex); - } - File networkDataFile = new File(openStackFolder, "network_data.json"); - try (FileWriter fw = new FileWriter(networkDataFile); BufferedWriter bw = new BufferedWriter(fw)) { - bw.write("{}"); - } catch (IOException ex) { - s_logger.error("Failed to create file ", ex); - return new Answer(cmd, ex); - } - } else { - s_logger.error("Failed to create folder " + openStackFolder); - return new Answer(cmd, false, "Failed to create folder " + openStackFolder); - } - - JsonObject metaData = new JsonObject(); - for (String[] item : cmd.getVmData()) { - String dataType = item[CONFIGDATA_DIR]; - String fileName = item[CONFIGDATA_FILE]; - String content = item[CONFIGDATA_CONTENT]; - s_logger.debug(String.format("[createConfigDriveIsoForVM] dataType=%s, filename=%s, content=%s", - dataType, fileName, (fileName.equals(PASSWORD_FILE)?"********":content))); - - // create file with content in folder - if (dataType != null && !dataType.isEmpty()) { - //create folder - File typeFolder = new File(tempDirName + cloudStackConfigDriveName + dataType); - if (typeFolder.exists() || typeFolder.mkdirs()) { - if (StringUtils.isNotEmpty(content)) { - File file = new File(typeFolder, fileName + ".txt"); - try { - if (fileName.equals(USERDATA_FILE)) { - // User Data is passed as a base64 encoded string - FileUtils.writeByteArrayToFile(file, Base64.decodeBase64(content)); - } else { - FileUtils.write(file, content, com.cloud.utils.StringUtils.getPreferredCharset()); - } - } catch (IOException ex) { - s_logger.error("Failed to create file ", ex); - return new Answer(cmd, ex); - } - } - } else { - s_logger.error("Failed to create folder " + typeFolder); - return new Answer(cmd, false, "Failed to create folder " + typeFolder); - } - - //now write the file to the OpenStack directory - metaData = constructOpenStackMetaData(metaData, dataType, fileName, content); - } - } - - File metaDataFile = new File(openStackFolder, "meta_data.json"); - try (FileWriter fw = new FileWriter(metaDataFile); BufferedWriter bw = new BufferedWriter(fw)) { - bw.write(metaData.toString()); - } catch (IOException ex) { - s_logger.error("Failed to create file ", ex); - return new Answer(cmd, ex); - } - - String linkResult = linkUserData(tempDirName); - if (linkResult != null) { - String errMsg = "Unable to create user_data link due to " + linkResult; - s_logger.warn(errMsg); - return new Answer(cmd, false, errMsg); - } - - File tmpIsoStore = new File(tempDirName, new File(cmd.getIsoFile()).getName()); - Script command = new Script(!_inSystemVM, "/usr/bin/genisoimage", _timeout, s_logger); - command.add("-o", tmpIsoStore.getAbsolutePath()); - command.add("-ldots"); - command.add("-allow-lowercase"); - command.add("-allow-multidot"); - command.add("-cache-inodes"); // Enable caching inode and device numbers to find hard links to files. - command.add("-l"); - command.add("-quiet"); - command.add("-J"); - command.add("-r"); - command.add("-V", cmd.getConfigDriveLabel()); - command.add(tempDirName); - s_logger.debug("execute command: " + command.toString()); - String result = command.execute(); - if (result != null) { - String errMsg = "Unable to create iso file: " + cmd.getIsoFile() + " due to " + result; - s_logger.warn(errMsg); - return new Answer(cmd, false, errMsg); - } - copyLocalToNfs(tmpIsoStore, new File(cmd.getIsoFile()), cmd.getDestStore()); - - } catch (IOException e) { - return new Answer(cmd, e); - } catch (ConfigurationException e) { - s_logger.warn("SecondStorageException ", e); - return new Answer(cmd, e); - } finally { - try { - FileUtils.deleteDirectory(tempDir.toFile()); - } catch (IOException ioe) { - s_logger.warn("Failed to delete ConfigDrive temporary directory: " + tempDirName, ioe); - } - } - } - return new Answer(cmd); - } - - JsonObject constructOpenStackMetaData(JsonObject metaData, String dataType, String fileName, String content) { - if (dataType.equals(NetworkModel.METATDATA_DIR) && StringUtils.isNotEmpty(content)) { - //keys are a special case in OpenStack format - if (NetworkModel.PUBLIC_KEYS_FILE.equals(fileName)) { - String[] keyArray = content.replace("\\n", "").split(" "); - String keyName = "key"; - if (keyArray.length > 3 && StringUtils.isNotEmpty(keyArray[2])){ - keyName = keyArray[2]; - } - - JsonObject keyLegacy = new JsonObject(); - keyLegacy.addProperty("type", "ssh"); - keyLegacy.addProperty("data", content.replace("\\n", "")); - keyLegacy.addProperty("name", keyName); - metaData.add("keys", arrayOf(keyLegacy)); - - JsonObject key = new JsonObject(); - key.addProperty(keyName, content); - metaData.add("public_keys", key); - } else if (NetworkModel.openStackFileMapping.get(fileName) != null) { - metaData.addProperty(NetworkModel.openStackFileMapping.get(fileName), content); - } - } - return metaData; + return configDriveFactory.executeRequest(cmd); } - private static JsonArray arrayOf(JsonElement... elements) { - JsonArray array = new JsonArray(); - for (JsonElement element : elements) { - array.add(element); - } - return array; - } - private String linkUserData(String tempDirName) { - //Hard link the user_data.txt file with the user_data file in the OpenStack directory. - String userDataFilePath = tempDirName + cloudStackConfigDriveName + "userdata/user_data.txt"; - if ((new File(userDataFilePath).exists())) { - Script hardLink = new Script(!_inSystemVM, "ln", _timeout, s_logger); - hardLink.add(userDataFilePath); - hardLink.add(tempDirName + openStackConfigDriveName + "user_data"); - s_logger.debug("execute command: " + hardLink.toString()); - return hardLink.execute(); - } - return null; - } - protected void copyLocalToNfs(File localFile, File isoFile, DataStoreTO destData) throws ConfigurationException, IOException { - String scriptsDir = "scripts/storage/secondary"; - String createVolScr = Script.findScript(scriptsDir, "createvolume.sh"); - if (createVolScr == null) { - throw new ConfigurationException("Unable to find createvolume.sh"); - } - s_logger.info("createvolume.sh found in " + createVolScr); - int installTimeoutPerGig = 180 * 60 * 1000; - int imgSizeGigs = (int) Math.ceil(localFile.length() * 1.0d / (1024 * 1024 * 1024)); - imgSizeGigs++; // add one just in case - long timeout = imgSizeGigs * installTimeoutPerGig; - Script scr = new Script(createVolScr, timeout, s_logger); - scr.add("-s", Integer.toString(imgSizeGigs)); - scr.add("-n", isoFile.getName()); - scr.add("-t", getRootDir(destData.getUrl(), _nfsVersion) + "/" + isoFile.getParent()); - scr.add("-f", localFile.getAbsolutePath()); - scr.add("-d", "configDrive"); - String result; - result = scr.execute(); - - if (result != null) { - // script execution failure - throw new CloudRuntimeException("Failed to run script " + createVolScr); - } - } public Answer execute(GetDatadisksCommand cmd) { DataTO srcData = cmd.getData(); @@ -658,7 +336,7 @@ public Answer execute(GetDatadisksCommand cmd) { try { String secondaryMountPoint = getRootDir(secondaryStorageUrl, _nfsVersion); - s_logger.info("MDOVE Secondary storage mount point: " + secondaryMountPoint); + s_logger.info("MOVE Secondary storage mount point: " + secondaryMountPoint); String srcOVAFileName = getTemplateOnSecStorageFilePath(secondaryMountPoint, templateRelativeFolderPath, templateInfo.second(), ImageFormat.OVA.getFileExtension()); @@ -2684,16 +2362,16 @@ protected Answer deleteVolume(final DeleteCommand cmd) { } @Override - synchronized public String getRootDir(String secUrl, Integer nfsVersion) { + synchronized public String getRootDir(String url, Integer nfsVersion) { if (!_inSystemVM) { return _parent; } try { - URI uri = new URI(secUrl); + URI uri = new URI(url); String dir = mountUri(uri, nfsVersion); return _parent + "/" + dir; } catch (Exception e) { - String msg = "GetRootDir for " + secUrl + " failed due to " + e.toString(); + String msg = "GetRootDir for " + url + " failed due to " + e.toString(); s_logger.error(msg, e); throw new CloudRuntimeException(msg); } @@ -2759,7 +2437,7 @@ public boolean configure(String name, Map params) throws Configu String inSystemVM = (String)params.get("secondary.storage.vm"); if (inSystemVM == null || "true".equalsIgnoreCase(inSystemVM)) { s_logger.debug("conf secondary.storage.vm is true, act as if executing in SSVM"); - _inSystemVM = true; + setInSystemVM(true); } _storageIp = (String)params.get("storageip"); @@ -2850,8 +2528,9 @@ public boolean configure(String name, Map params) throws Configu _nfsVersion = retrieveNfsVersionFromParams(params); } + configDriveFactory = new ConfigDriveFactory(_nfsVersion, this); + _params.put(StorageLayer.InstanceConfigKey, _storage); try { - _params.put(StorageLayer.InstanceConfigKey, _storage); _dlMgr = new DownloadManagerImpl(); _dlMgr.configure("DownloadManager", _params); _upldMgr = new UploadManagerImpl(); @@ -3047,7 +2726,8 @@ protected String mountUri(URI uri, Integer nfsVersion) throws UnknownHostExcepti return dir; } - protected void umount(String localRootPath, URI uri) { + @Override + public void umount(String localRootPath, URI uri) { ensureLocalRootPathExists(localRootPath, uri); if (!mountExists(localRootPath, uri)) { @@ -3070,7 +2750,8 @@ protected void umount(String localRootPath, URI uri) { s_logger.debug("Successfully umounted " + localRootPath); } - protected void mount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion) { + @Override + public void mount(String localRootPath, String remoteDevice, URI uri, Integer nfsVersion) { s_logger.debug("mount " + uri.toString() + " on " + localRootPath + ((nfsVersion != null) ? " nfsVersion=" + nfsVersion : "")); ensureLocalRootPathExists(localRootPath, uri);