[ https://issues.apache.org/jira/browse/CLOUDSTACK-10290?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16482315#comment-16482315 ]
ASF GitHub Bot commented on CLOUDSTACK-10290: --------------------------------------------- rhtyd closed pull request #2651: CLOUDSTACK-10290: Introduce option to have config drives on primary storage URL: https://github.com/apache/cloudstack/pull/2651 This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): diff --git a/api/src/com/cloud/network/element/UserDataServiceProvider.java b/api/src/com/cloud/network/element/UserDataServiceProvider.java index 45ab0d877a3..bf6d7e819e9 100644 --- a/api/src/com/cloud/network/element/UserDataServiceProvider.java +++ b/api/src/com/cloud/network/element/UserDataServiceProvider.java @@ -26,7 +26,7 @@ import com.cloud.vm.VirtualMachineProfile; public interface UserDataServiceProvider extends NetworkElement { - public boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) + boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException; boolean savePassword(Network network, NicProfile nic, VirtualMachineProfile vm) throws ResourceUnavailableException; diff --git a/client/pom.xml b/client/pom.xml index 5653f536ac6..60402759576 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -378,6 +378,11 @@ <artifactId>cloud-engine-storage-cache</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-engine-storage-configdrive</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.cloudstack</groupId> <artifactId>cloud-controller-secondary-storage</artifactId> diff --git a/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java b/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java index d6d87d48c05..3d8d8f7e10e 100644 --- a/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java +++ b/core/src/com/cloud/agent/api/HandleConfigDriveIsoCommand.java @@ -19,28 +19,22 @@ package com.cloud.agent.api; -import java.util.List; - import com.cloud.agent.api.to.DataStoreTO; public class HandleConfigDriveIsoCommand extends Command { - String isoFile; - List<String[]> vmData; - String configDriveLabel; - boolean create = false; - private boolean update = false; - private DataStoreTO destStore; - - public HandleConfigDriveIsoCommand(List<String[]> vmData, String label, DataStoreTO destStore, String isoFile, boolean create, boolean update) { - this.vmData = vmData; - this.configDriveLabel = label; - this.create = create; - this.update = update; - this.destStore = destStore; + @LogLevel(LogLevel.Log4jLevel.Off) + private String isoData; + private String isoFile; + private boolean create = false; + private DataStoreTO destStore; + public HandleConfigDriveIsoCommand(String isoFile, String isoData, DataStoreTO destStore, boolean create) { this.isoFile = isoFile; + this.isoData = isoData; + this.destStore = destStore; + this.create = create; } @Override @@ -48,22 +42,14 @@ public boolean executeInSequence() { return false; } - public List<String[]> getVmData() { - return vmData; - } - - public void setVmData(List<String[]> vmData) { - this.vmData = vmData; + public String getIsoData() { + return isoData; } public boolean isCreate() { return create; } - public String getConfigDriveLabel() { - return configDriveLabel; - } - public DataStoreTO getDestStore() { return destStore; } @@ -71,8 +57,4 @@ public DataStoreTO getDestStore() { public String getIsoFile() { return isoFile; } - - public boolean isUpdate() { - return update; - } } diff --git a/engine/api/src/com/cloud/vm/VirtualMachineManager.java b/engine/api/src/com/cloud/vm/VirtualMachineManager.java index 556ec86f060..6041146b9de 100644 --- a/engine/api/src/com/cloud/vm/VirtualMachineManager.java +++ b/engine/api/src/com/cloud/vm/VirtualMachineManager.java @@ -49,14 +49,17 @@ */ public interface VirtualMachineManager extends Manager { - static final ConfigKey<Boolean> ExecuteInSequence = new ConfigKey<Boolean>("Advanced", Boolean.class, "execute.in.sequence.hypervisor.commands", "false", + ConfigKey<Boolean> ExecuteInSequence = new ConfigKey<>("Advanced", Boolean.class, "execute.in.sequence.hypervisor.commands", "false", "If set to true, start, stop, reboot, copy and migrate commands will be serialized on the agent side. If set to false the commands are executed in parallel. Default value is false.", false); - static final ConfigKey<String> VmConfigDriveLabel = new ConfigKey<String>("Hidden", String.class, "vm.configdrive.label", "config-2", + ConfigKey<String> VmConfigDriveLabel = new ConfigKey<>("Hidden", String.class, "vm.configdrive.label", "config-2", "The default label name for the config drive", false); - public interface Topics { - public static final String VM_POWER_STATE = "vm.powerstate"; + ConfigKey<Boolean> VmConfigDriveOnPrimaryPool = new ConfigKey<>("Advanced", Boolean.class, "vm.configdrive.primarypool.enabled", "false", + "If config drive need to be created and hosted on primary storage pool. Currently only supported for KVM.", true); + + interface Topics { + String VM_POWER_STATE = "vm.powerstate"; } /** diff --git a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java index 8b3071fb695..6f2ae852e12 100755 --- a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java @@ -1108,10 +1108,11 @@ public void orchestrateStart(final String vmUuid, final Map<VirtualMachineProfil } try { - _networkMgr.prepare(vmProfile, new DeployDestination(dest.getDataCenter(), dest.getPod(), null, null), ctx); + _networkMgr.prepare(vmProfile, new DeployDestination(dest.getDataCenter(), dest.getPod(), null, null, dest.getStorageForDisks()), ctx); if (vm.getHypervisorType() != HypervisorType.BareMetal) { volumeMgr.prepare(vmProfile, dest); } + //since StorageMgr succeeded in volume creation, reuse Volume for further tries until current cluster has capacity if (!reuseVolume) { reuseVolume = true; @@ -4018,7 +4019,7 @@ public String getConfigComponentName() { public ConfigKey<?>[] getConfigKeys() { return new ConfigKey<?>[] {ClusterDeltaSyncInterval, StartRetry, VmDestroyForcestop, VmOpCancelInterval, VmOpCleanupInterval, VmOpCleanupWait, VmOpLockStateRetry, - VmOpWaitInterval, ExecuteInSequence, VmJobCheckInterval, VmJobTimeout, VmJobStateReportInterval, VmConfigDriveLabel, HaVmRestartHostUp}; + VmOpWaitInterval, ExecuteInSequence, VmJobCheckInterval, VmJobTimeout, VmJobStateReportInterval, VmConfigDriveLabel, VmConfigDriveOnPrimaryPool, HaVmRestartHostUp}; } public List<StoragePoolAllocator> getStoragePoolAllocators() { diff --git a/engine/pom.xml b/engine/pom.xml index be86f1c9dc1..4fc66d12a60 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -53,6 +53,7 @@ <module>storage/datamotion</module> <module>storage/cache</module> <module>storage/snapshot</module> + <module>storage/configdrive</module> <module>components-api</module> <module>network</module> <module>service</module> diff --git a/engine/storage/configdrive/pom.xml b/engine/storage/configdrive/pom.xml new file mode 100644 index 00000000000..dc3d118eb68 --- /dev/null +++ b/engine/storage/configdrive/pom.xml @@ -0,0 +1,43 @@ +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <artifactId>cloud-engine-storage-configdrive</artifactId> + <name>Apache CloudStack Framework - Storage Config Drive Component</name> + <parent> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-engine</artifactId> + <version>4.11.1.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <dependencies> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-api</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-core</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + +</project> diff --git a/engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDrive.java b/engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDrive.java new file mode 100644 index 00000000000..ec461991537 --- /dev/null +++ b/engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDrive.java @@ -0,0 +1,36 @@ +// 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.configdrive; + +public class ConfigDrive { + + public final static String CONFIGDRIVEFILENAME = "configdrive.iso"; + public final static String CONFIGDRIVEDIR = "configdrive"; + + public static final String cloudStackConfigDriveName = "/cloudstack/"; + public static final String openStackConfigDriveName = "/openstack/latest/"; + + /** + * This is the path to iso file relative to mount point + * @return config drive iso file path + */ + public static String createConfigDrivePath(final String instanceName) { + return ConfigDrive.CONFIGDRIVEDIR + "/" + instanceName + "/" + ConfigDrive.CONFIGDRIVEFILENAME; + } + +} diff --git a/engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java b/engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java new file mode 100644 index 00000000000..d847aa1d1d7 --- /dev/null +++ b/engine/storage/configdrive/src/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilder.java @@ -0,0 +1,222 @@ +// 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.configdrive; + +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.PASSWORD_FILE; +import static com.cloud.network.NetworkModel.USERDATA_FILE; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.joda.time.Duration; + +import com.cloud.network.NetworkModel; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import com.google.common.base.Strings; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class ConfigDriveBuilder { + + public static final Logger LOG = Logger.getLogger(ConfigDriveBuilder.class); + + private static void writeFile(final File folder, final String file, final String content) { + if (folder == null || Strings.isNullOrEmpty(file)) { + return; + } + final File vendorDataFile = new File(folder, file); + try (final FileWriter fw = new FileWriter(vendorDataFile); final BufferedWriter bw = new BufferedWriter(fw)) { + bw.write(content); + } catch (IOException ex) { + throw new CloudRuntimeException("Failed to create config drive file " + file, ex); + } + } + + public static String fileToBase64String(final File isoFile) throws IOException { + byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(isoFile)); + return new String(encoded, StandardCharsets.US_ASCII); + } + + public static File base64StringToFile(final String encodedIsoData, final String folder, final String fileName) throws IOException { + byte[] decoded = Base64.decodeBase64(encodedIsoData.getBytes(StandardCharsets.US_ASCII)); + Path destPath = Paths.get(folder, fileName); + return Files.write(destPath, decoded).toFile(); + } + + public static String buildConfigDrive(final List<String[]> vmData, final String isoFileName, final String driveLabel) { + if (vmData == null) { + throw new CloudRuntimeException("No VM metadata provided"); + } + + Path tempDir = null; + String tempDirName = null; + try { + tempDir = Files.createTempDirectory(ConfigDrive.CONFIGDRIVEDIR); + tempDirName = tempDir.toString(); + + File openStackFolder = new File(tempDirName + ConfigDrive.openStackConfigDriveName); + if (openStackFolder.exists() || openStackFolder.mkdirs()) { + writeFile(openStackFolder, "vendor_data.json", "{}"); + writeFile(openStackFolder, "network_data.json", "{}"); + } else { + throw new CloudRuntimeException("Failed to create folder " + openStackFolder); + } + + JsonObject metaData = new JsonObject(); + for (String[] item : vmData) { + String dataType = item[CONFIGDATA_DIR]; + String fileName = item[CONFIGDATA_FILE]; + String content = item[CONFIGDATA_CONTENT]; + LOG.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 + ConfigDrive.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) { + throw new CloudRuntimeException("Failed to create file ", ex); + } + } + } else { + throw new CloudRuntimeException("Failed to create folder: " + typeFolder); + } + + //now write the file to the OpenStack directory + metaData = buildOpenStackMetaData(metaData, dataType, fileName, content); + } + } + writeFile(openStackFolder, "meta_data.json", metaData.toString()); + + String linkResult = linkUserData(tempDirName); + if (linkResult != null) { + String errMsg = "Unable to create user_data link due to " + linkResult; + throw new CloudRuntimeException(errMsg); + } + + File tmpIsoStore = new File(tempDirName, new File(isoFileName).getName()); + Script command = new Script("/usr/bin/genisoimage", Duration.standardSeconds(300), LOG); + 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", driveLabel); + command.add(tempDirName); + LOG.debug("Executing config drive creation command: " + command.toString()); + String result = command.execute(); + if (result != null) { + String errMsg = "Unable to create iso file: " + isoFileName + " due to " + result; + LOG.warn(errMsg); + throw new CloudRuntimeException(errMsg); + } + File tmpIsoFile = new File(tmpIsoStore.getAbsolutePath()); + // Max allowed file size of config drive is 64MB: https://docs.openstack.org/project-install-guide/baremetal/draft/configdrive.html + if (tmpIsoFile.length() > (64L * 1024L * 1024L)) { + throw new CloudRuntimeException("Config drive file exceeds maximum allowed size of 64MB"); + } + return fileToBase64String(tmpIsoFile); + } catch (IOException e) { + throw new CloudRuntimeException("Failed due to", e); + } finally { + try { + FileUtils.deleteDirectory(tempDir.toFile()); + } catch (IOException ioe) { + LOG.warn("Failed to delete ConfigDrive temporary directory: " + tempDirName, ioe); + } + } + } + + private static String linkUserData(String tempDirName) { + //Hard link the user_data.txt file with the user_data file in the OpenStack directory. + String userDataFilePath = tempDirName + ConfigDrive.cloudStackConfigDriveName + "userdata/user_data.txt"; + if ((new File(userDataFilePath).exists())) { + Script hardLink = new Script("ln", Duration.standardSeconds(300), LOG); + hardLink.add(userDataFilePath); + hardLink.add(tempDirName + ConfigDrive.openStackConfigDriveName + "user_data"); + LOG.debug("execute command: " + hardLink.toString()); + return hardLink.execute(); + } + return null; + } + + private static JsonArray arrayOf(JsonElement... elements) { + JsonArray array = new JsonArray(); + for (JsonElement element : elements) { + array.add(element); + } + return array; + } + + private static JsonObject buildOpenStackMetaData(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; + } + +} diff --git a/engine/storage/configdrive/test/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java b/engine/storage/configdrive/test/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java new file mode 100644 index 00000000000..50a4384d5c8 --- /dev/null +++ b/engine/storage/configdrive/test/org/apache/cloudstack/storage/configdrive/ConfigDriveBuilderTest.java @@ -0,0 +1,65 @@ +// 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.configdrive; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.Assert; +import org.junit.Test; + +public class ConfigDriveBuilderTest { + + @Test + public void testConfigDriveIsoPath() throws IOException { + Assert.assertEquals(ConfigDrive.createConfigDrivePath("i-x-y"), "configdrive/i-x-y/configdrive.iso"); + } + + @Test + public void testConfigDriveBuild() throws IOException { + List<String[]> actualVmData = Arrays.asList( + new String[]{"userdata", "user_data", "c29tZSB1c2VyIGRhdGE="}, + new String[]{"metadata", "service-offering", "offering"}, + new String[]{"metadata", "availability-zone", "zone1"}, + new String[]{"metadata", "local-hostname", "hostname"}, + new String[]{"metadata", "local-ipv4", "192.168.111.111"}, + new String[]{"metadata", "public-hostname", "7.7.7.7"}, + new String[]{"metadata", "public-ipv4", "7.7.7.7"}, + new String[]{"metadata", "vm-id", "uuid"}, + new String[]{"metadata", "instance-id", "i-x-y"}, + new String[]{"metadata", "public-keys", "ssh-rsa some-key"}, + new String[]{"metadata", "cloud-identifier", String.format("CloudStack-{%s}", "uuid")}, + new String[]{"password", "vm_password", "password123"} + ); + + final Path tempDir = Files.createTempDirectory(ConfigDrive.CONFIGDRIVEDIR); + final String isoData = ConfigDriveBuilder.buildConfigDrive(actualVmData, "i-x-y.iso", "config-2"); + final File isoFile = ConfigDriveBuilder.base64StringToFile(isoData, tempDir.toAbsolutePath().toString(), ConfigDrive.CONFIGDRIVEFILENAME); + + Assert.assertTrue(isoFile.exists()); + Assert.assertTrue(isoFile.isFile()); + Assert.assertTrue(isoFile.length() > 0L); + + FileUtils.deleteDirectory(tempDir.toFile()); + } +} \ No newline at end of file diff --git a/packaging/centos7/cloud.spec b/packaging/centos7/cloud.spec index 525421c0108..96b679c8955 100644 --- a/packaging/centos7/cloud.spec +++ b/packaging/centos7/cloud.spec @@ -75,7 +75,7 @@ Requires: sudo Requires: /sbin/service Requires: /sbin/chkconfig Requires: /usr/bin/ssh-keygen -Requires: mkisofs +Requires: genisoimage Requires: mysql-connector-python Requires: ipmitool Requires: %{name}-common = %{_ver} diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml index d90e79adfc9..1d0dd66cf87 100644 --- a/plugins/hypervisors/kvm/pom.xml +++ b/plugins/hypervisors/kvm/pom.xml @@ -39,6 +39,11 @@ <artifactId>cloud-plugin-network-ovs</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-engine-storage-configdrive</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>com.ceph</groupId> <artifactId>rados</artifactId> 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 fc5e5395b87..424280cc66c 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 @@ -2200,9 +2200,8 @@ public String getVolumePath(final Connect conn, final DiskTO volume) throws Libv final DataTO data = volume.getData(); final DataStoreTO store = data.getDataStore(); - if (volume.getType() == Volume.Type.ISO && data.getPath() != null) { - final NfsTO nfsStore = (NfsTO)store; - final String isoPath = nfsStore.getUrl() + File.separator + data.getPath(); + if (volume.getType() == Volume.Type.ISO && data.getPath() != null && (store instanceof NfsTO || store instanceof PrimaryDataStoreTO)) { + final String isoPath = store.getUrl().split("\\?")[0] + File.separator + data.getPath(); final int index = isoPath.lastIndexOf("/"); final String path = isoPath.substring(0, index); final String name = isoPath.substring(index + 1); 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 00000000000..5e4ef485123 --- /dev/null +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtHandleConfigDriveCommandWrapper.java @@ -0,0 +1,79 @@ +// 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 java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.HandleConfigDriveIsoCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; + +@ResourceWrapper(handles = HandleConfigDriveIsoCommand.class) +public final class LibvirtHandleConfigDriveCommandWrapper extends CommandWrapper<HandleConfigDriveIsoCommand, Answer, LibvirtComputingResource> { + private static final Logger LOG = Logger.getLogger(LibvirtHandleConfigDriveCommandWrapper.class); + + @Override + public Answer execute(final HandleConfigDriveIsoCommand command, final LibvirtComputingResource libvirtComputingResource) { + final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr(); + final KVMStoragePool pool = storagePoolMgr.getStoragePool(Storage.StoragePoolType.NetworkFilesystem, command.getDestStore().getUuid()); + if (pool == null) { + return new Answer(command, false, "Pool not found, config drive for KVM is only supported for NFS"); + } + + final String mountPoint = pool.getLocalPath(); + final Path isoPath = Paths.get(mountPoint, command.getIsoFile()); + final File isoFile = new File(mountPoint, command.getIsoFile()); + if (command.isCreate()) { + LOG.debug("Creating config drive: " + command.getIsoFile()); + if (command.getIsoData() == null) { + return new Answer(command, false, "Invalid config drive ISO data received"); + } + if (isoFile.exists()) { + LOG.debug("An old config drive iso already exists"); + } + try { + Files.createDirectories(isoPath.getParent()); + ConfigDriveBuilder.base64StringToFile(command.getIsoData(), mountPoint, command.getIsoFile()); + } catch (IOException e) { + return new Answer(command, false, "Failed due to exception: " + e.getMessage()); + } + } else { + try { + FileUtils.deleteDirectory(isoPath.getParent().toFile()); + } catch (IOException e) { + LOG.warn("Failed to delete config drive: " + isoPath.toAbsolutePath().toString()); + return new Answer(command, false, "Failed due to exception: " + e.getMessage()); + } + } + + return new Answer(command); + } +} \ No newline at end of file diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java index c6135080671..4d0523c9a06 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/KVMStoragePoolManager.java @@ -265,7 +265,7 @@ public KVMStoragePool getStoragePoolByURI(String uri) { String uuid = null; String sourceHost = ""; StoragePoolType protocol = null; - if (storageUri.getScheme().equalsIgnoreCase("nfs")) { + if (storageUri.getScheme().equalsIgnoreCase("nfs") || storageUri.getScheme().equalsIgnoreCase("NetworkFilesystem")) { sourcePath = storageUri.getPath(); sourcePath = sourcePath.replace("//", "/"); sourceHost = storageUri.getHost(); diff --git a/server/pom.xml b/server/pom.xml index 534b1a34c70..55d0a6e247f 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -141,6 +141,11 @@ <artifactId>cloud-framework-agent-lb</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-engine-storage-configdrive</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.opensaml</groupId> <artifactId>opensaml</artifactId> diff --git a/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java b/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java index 585b443557b..dab4a14311c 100644 --- a/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java +++ b/server/src/com/cloud/network/element/ConfigDriveNetworkElement.java @@ -20,20 +20,20 @@ import java.util.List; import java.util.Map; import java.util.Set; -import javax.inject.Inject; -import org.apache.log4j.Logger; +import javax.inject.Inject; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.storage.configdrive.ConfigDrive; +import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder; import org.apache.cloudstack.storage.to.TemplateObjectTO; +import org.apache.log4j.Logger; + import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; -import com.cloud.agent.api.AttachIsoAnswer; -import com.cloud.agent.api.AttachIsoCommand; import com.cloud.agent.api.HandleConfigDriveIsoCommand; import com.cloud.agent.api.to.DiskTO; import com.cloud.configuration.ConfigurationManager; @@ -41,10 +41,8 @@ import com.cloud.deploy.DeployDestination; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientCapacityException; -import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.exception.UnsupportedServiceException; -import com.cloud.host.Host; import com.cloud.host.dao.HostDao; import com.cloud.network.Network; import com.cloud.network.Network.Capability; @@ -54,54 +52,48 @@ import com.cloud.network.NetworkModel; import com.cloud.network.Networks.TrafficType; import com.cloud.network.PhysicalNetworkServiceProvider; -import com.cloud.network.dao.NetworkDao; import com.cloud.offering.NetworkOffering; import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.DataStoreRole; import com.cloud.storage.Storage; +import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.GuestOSCategoryDao; import com.cloud.storage.dao.GuestOSDao; +import com.cloud.storage.dao.VolumeDao; import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.StateListener; import com.cloud.utils.fsm.StateMachine2; import com.cloud.vm.Nic; import com.cloud.vm.NicProfile; import com.cloud.vm.ReservationContext; import com.cloud.vm.UserVmDetailVO; -import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.VirtualMachineProfile; -import com.cloud.vm.dao.DomainRouterDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; public class ConfigDriveNetworkElement extends AdapterBase implements NetworkElement, UserDataServiceProvider, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualMachine>, NetworkMigrationResponder { - private static final Logger s_logger = Logger.getLogger(ConfigDriveNetworkElement.class); + private static final Logger LOG = Logger.getLogger(ConfigDriveNetworkElement.class); private static final Map<Service, Map<Capability, String>> capabilities = setCapabilities(); - @Inject - NetworkDao _networkConfigDao; @Inject NetworkModel _networkMgr; @Inject - UserVmManager _userVmMgr; - @Inject UserVmDao _userVmDao; @Inject UserVmDetailsDao _userVmDetailsDao; @Inject - DomainRouterDao _routerDao; - @Inject ConfigurationManager _configMgr; @Inject DataCenterDao _dcDao; @Inject - AgentManager _agentManager; - @Inject ServiceOfferingDao _serviceOfferingDao; @Inject NetworkModel _networkModel; @@ -110,16 +102,16 @@ @Inject GuestOSDao _guestOSDao; @Inject + VolumeDao _volumeDao; + @Inject HostDao _hostDao; @Inject + AgentManager agentManager; + @Inject DataStoreManager _dataStoreMgr; @Inject EndPointSelector _ep; - @Inject - VolumeOrchestrationService _volumeMgr; - private final static String CONFIGDRIVEFILENAME = "configdrive.iso"; - private final static String CONFIGDRIVEDIR = "ConfigDrive"; private final static Integer CONFIGDRIVEDISKSEQ = 4; private boolean canHandle(TrafficType trafficType) { @@ -149,20 +141,13 @@ public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm if (!nic.isDefaultNic()) { return true; } - // Remove form secondary storage - DataStore secondaryStore = _dataStoreMgr.getImageStore(network.getDataCenterId()); - - 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); - if (endpoint == null) { - s_logger.error(String.format("Secondary store: %s not available", secondaryStore.getName())); + + try { + return deleteConfigDriveIso(vm.getVirtualMachine()); + } catch (ResourceUnavailableException e) { + LOG.error("Failed to delete config drive due to: ", e); return false; } - Answer answer = endpoint.sendMessage(deleteCommand); - return answer.getResult(); } @Override @@ -207,37 +192,49 @@ public boolean canEnableIndividualServices() { } private String getSshKey(VirtualMachineProfile profile) { - UserVmDetailVO vmDetailSshKey = _userVmDetailsDao.findDetail(profile.getId(), "SSH.PublicKey"); + final UserVmDetailVO vmDetailSshKey = _userVmDetailsDao.findDetail(profile.getId(), "SSH.PublicKey"); return (vmDetailSshKey!=null ? vmDetailSshKey.getValue() : null); } @Override public boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, InsufficientCapacityException, ResourceUnavailableException { - String sshPublicKey = getSshKey(profile); return (canHandle(network.getTrafficType()) - && updateConfigDrive(profile, sshPublicKey, nic)) - && updateConfigDriveIso(network, profile, dest.getHost(), false); + && configureConfigDriveData(profile, nic)) + && createConfigDriveIso(profile, dest); } @Override - public boolean savePassword(Network network, NicProfile nic, VirtualMachineProfile profile) throws ResourceUnavailableException { - String sshPublicKey = getSshKey(profile); - if (!(canHandle(network.getTrafficType()) && updateConfigDrive(profile, sshPublicKey, nic))) return false; - return updateConfigDriveIso(network, profile, true); + public boolean savePassword(final Network network, final NicProfile nic, final VirtualMachineProfile vm) throws ResourceUnavailableException { + // savePassword is called by resetPasswordForVirtualMachine API which requires VM to be shutdown + // Upper layers should save password in db, we do not need to update/create config drive iso at this point + // Config drive will be created with updated password when VM starts in future + if (vm != null && vm.getVirtualMachine().getState().equals(VirtualMachine.State.Running)) { + throw new CloudRuntimeException("VM should to stopped to reset password"); + } + return canHandle(network.getTrafficType()); } @Override - public boolean saveSSHKey(Network network, NicProfile nic, VirtualMachineProfile vm, String sshPublicKey) throws ResourceUnavailableException { - if (!(canHandle(network.getTrafficType()) && updateConfigDrive(vm, sshPublicKey, nic))) return false; - return updateConfigDriveIso(network, vm, true); + public boolean saveSSHKey(final Network network, final NicProfile nic, final VirtualMachineProfile vm, final String sshPublicKey) throws ResourceUnavailableException { + // saveSSHKey is called by resetSSHKeyForVirtualMachine API which requires VM to be shutdown + // Upper layers should save ssh public key in db, we do not need to update/create config drive iso at this point + // Config drive will be created with updated password when VM starts in future + if (vm != null && vm.getVirtualMachine().getState().equals(VirtualMachine.State.Running)) { + throw new CloudRuntimeException("VM should to stopped to reset password"); + } + return canHandle(network.getTrafficType()); } @Override - public boolean saveUserData(Network network, NicProfile nic, VirtualMachineProfile profile) throws ResourceUnavailableException { - String sshPublicKey = getSshKey(profile); - if (!(canHandle(network.getTrafficType()) && updateConfigDrive(profile, sshPublicKey, nic))) return false; - return updateConfigDriveIso(network, profile, true); + public boolean saveUserData(final Network network, final NicProfile nic, final VirtualMachineProfile vm) throws ResourceUnavailableException { + // saveUserData is called by updateVirtualMachine API which requires VM to be shutdown + // Upper layers should save userdata in db, we do not need to update/create config drive iso at this point + // Config drive will be created with updated password when VM starts in future + if (vm != null && vm.getVirtualMachine().getState().equals(VirtualMachine.State.Running)) { + throw new CloudRuntimeException("VM should to stopped to reset password"); + } + return canHandle(network.getTrafficType()); } @Override @@ -251,30 +248,22 @@ public boolean preStateTransitionEvent(VirtualMachine.State oldState, VirtualMac } @Override - public boolean postStateTransitionEvent(StateMachine2.Transition<VirtualMachine.State, VirtualMachine.Event> transition, VirtualMachine vo, boolean status, Object opaque) { + public boolean postStateTransitionEvent(StateMachine2.Transition<VirtualMachine.State, VirtualMachine.Event> transition, VirtualMachine vm, boolean status, Object opaque) { if (transition.getToState().equals(VirtualMachine.State.Expunging) && transition.getEvent().equals(VirtualMachine.Event.ExpungeOperation)) { - Nic nic = _networkModel.getDefaultNic(vo.getId()); + Nic nic = _networkModel.getDefaultNic(vm.getId()); + if (nic == null) { + return true; + } try { - if (nic != null) { - final Network network = _networkMgr.getNetwork(nic.getNetworkId()); - final UserDataServiceProvider userDataUpdateProvider = _networkModel.getUserDataUpdateProvider(network); - final Provider provider = userDataUpdateProvider.getProvider(); - if (provider.equals(Provider.ConfigDrive)) { - // Delete config drive ISO on destroy - DataStore secondaryStore = _dataStoreMgr.getImageStore(vo.getDataCenterId()); - String isoFile = "/" + CONFIGDRIVEDIR + "/" + vo.getInstanceName() + "/" + CONFIGDRIVEFILENAME; - HandleConfigDriveIsoCommand deleteCommand = new HandleConfigDriveIsoCommand(null, - null, secondaryStore.getTO(), isoFile, false, false); - EndPoint endpoint = _ep.select(secondaryStore); - if (endpoint == null) { - s_logger.error(String.format("Secondary store: %s not available", secondaryStore.getName())); - return false; - } - Answer answer = endpoint.sendMessage(deleteCommand); - if (!answer.getResult()) { - s_logger.error(String.format("Update ISO failed, details: %s", answer.getDetails())); - return false; - } + final Network network = _networkMgr.getNetwork(nic.getNetworkId()); + final UserDataServiceProvider userDataUpdateProvider = _networkModel.getUserDataUpdateProvider(network); + final Provider provider = userDataUpdateProvider.getProvider(); + if (provider.equals(Provider.ConfigDrive)) { + try { + return deleteConfigDriveIso(vm); + } catch (ResourceUnavailableException e) { + LOG.error("Failed to delete config drive due to: ", e); + return false; } } } catch (UnsupportedServiceException usse) {} @@ -285,9 +274,9 @@ public boolean postStateTransitionEvent(StateMachine2.Transition<VirtualMachine. @Override public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) { if (nic.isDefaultNic() && _networkModel.getUserDataUpdateProvider(network).getProvider().equals(Provider.ConfigDrive)) { - s_logger.trace(String.format("[prepareMigration] for vm: %s", vm.getInstanceName())); - DataStore secondaryStore = _dataStoreMgr.getImageStore(network.getDataCenterId()); - configureConfigDriveDisk(vm, secondaryStore); + LOG.trace(String.format("[prepareMigration] for vm: %s", vm.getInstanceName())); + final DataStore dataStore = findDataStore(vm, dest); + addConfigDriveDisk(vm, dataStore); return false; } else return true; @@ -295,64 +284,114 @@ public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineP @Override public void rollbackMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { - } @Override public void commitMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) { + } + private DataStore findDataStore(VirtualMachineProfile profile, DeployDestination dest) { + DataStore dataStore = null; + if (VirtualMachineManager.VmConfigDriveOnPrimaryPool.value()) { + if (dest.getStorageForDisks() != null) { + for (final Volume volume : dest.getStorageForDisks().keySet()) { + if (volume.getVolumeType() == Volume.Type.ROOT) { + final StoragePool primaryPool = dest.getStorageForDisks().get(volume); + dataStore = _dataStoreMgr.getDataStore(primaryPool.getId(), DataStoreRole.Primary); + break; + } + } + } + if (dataStore == null) { + final List<VolumeVO> volumes = _volumeDao.findByInstanceAndType(profile.getVirtualMachine().getId(), Volume.Type.ROOT); + if (volumes != null && volumes.size() > 0) { + dataStore = _dataStoreMgr.getDataStore(volumes.get(0).getPoolId(), DataStoreRole.Primary); + } + } + } else { + dataStore = _dataStoreMgr.getImageStore(dest.getDataCenter().getId()); + } + return dataStore; } - private boolean updateConfigDriveIso(Network network, VirtualMachineProfile profile, boolean update) throws ResourceUnavailableException { - return updateConfigDriveIso(network, profile, null, update); + private Long findAgentIdForImageStore(final DataStore dataStore) throws ResourceUnavailableException { + EndPoint endpoint = _ep.select(dataStore); + if (endpoint == null) { + throw new ResourceUnavailableException("Config drive creation failed, secondary store not available", + dataStore.getClass(), dataStore.getId()); + } + return endpoint.getId(); } - private boolean updateConfigDriveIso(Network network, VirtualMachineProfile profile, Host host, boolean update) throws ResourceUnavailableException { - Integer deviceKey = null; - Long hostId; - if (host == null) { - hostId = (profile.getVirtualMachine().getHostId() == null ? profile.getVirtualMachine().getLastHostId(): profile.getVirtualMachine().getHostId()); + private Long findAgentId(VirtualMachineProfile profile, DeployDestination dest, DataStore dataStore) throws ResourceUnavailableException { + Long agentId; + if (dest.getHost() == null) { + agentId = (profile.getVirtualMachine().getHostId() == null ? profile.getVirtualMachine().getLastHostId() : profile.getVirtualMachine().getHostId()); } else { - hostId = host.getId(); + agentId = dest.getHost().getId(); + } + if (!VirtualMachineManager.VmConfigDriveOnPrimaryPool.value()) { + agentId = findAgentIdForImageStore(dataStore); } + return agentId; + } - DataStore secondaryStore = _dataStoreMgr.getImageStore(network.getDataCenterId()); - // Detach the existing ISO file if the machine is running - if (update && profile.getVirtualMachine().getState().equals(VirtualMachine.State.Running)) { - s_logger.debug("Detach config drive ISO for vm " + profile.getInstanceName() + " in host " + _hostDao.findById(hostId)); - deviceKey = detachIso(secondaryStore, profile.getInstanceName(), hostId); + private boolean createConfigDriveIso(VirtualMachineProfile profile, DeployDestination dest) throws ResourceUnavailableException { + final DataStore dataStore = findDataStore(profile, dest); + final Long agentId = findAgentId(profile, dest, dataStore); + if (agentId == null || dataStore == null) { + throw new ResourceUnavailableException("Config drive iso creation failed, agent or datastore not available", + ConfigDriveNetworkElement.class, 0L); } - // Create/Update the iso on the secondary store - s_logger.debug(String.format("%s config drive ISO for vm %s in host %s", - (update?"update":"create"), profile.getInstanceName(), _hostDao.findById(hostId).getName())); - EndPoint endpoint = _ep.select(secondaryStore); - if (endpoint == null) { - throw new ResourceUnavailableException(String.format("%s failed, secondary store not available", (update ? "Update" : "Create")), secondaryStore.getClass(), - secondaryStore.getId()); + LOG.debug("Creating config drive ISO for vm: " + profile.getInstanceName()); + + final String isoPath = ConfigDrive.createConfigDrivePath(profile.getInstanceName()); + final String isoData = ConfigDriveBuilder.buildConfigDrive(profile.getVmData(), ConfigDrive.CONFIGDRIVEFILENAME, profile.getConfigDriveLabel()); + final HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(isoPath, isoData, dataStore.getTO(), true); + + final Answer answer = agentManager.easySend(agentId, configDriveIsoCommand); + if (!answer.getResult()) { + throw new ResourceUnavailableException(String.format("Config drive iso creation failed, details: %s", + answer.getDetails()), ConfigDriveNetworkElement.class, 0L); + } + addConfigDriveDisk(profile, dataStore); + return true; + } + + private boolean deleteConfigDriveIso(final VirtualMachine vm) throws ResourceUnavailableException { + DataStore dataStore = _dataStoreMgr.getImageStore(vm.getDataCenterId()); + Long agentId = findAgentIdForImageStore(dataStore); + + if (VirtualMachineManager.VmConfigDriveOnPrimaryPool.value()) { + List<VolumeVO> volumes = _volumeDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT); + if (volumes != null && volumes.size() > 0) { + dataStore = _dataStoreMgr.getDataStore(volumes.get(0).getPoolId(), DataStoreRole.Primary); + } + agentId = (vm.getHostId() != null) ? vm.getHostId() : vm.getLastHostId(); } - String isoPath = CONFIGDRIVEDIR + "/" + profile.getInstanceName() + "/" + CONFIGDRIVEFILENAME; - HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(profile.getVmData(), - profile.getConfigDriveLabel(), secondaryStore.getTO(), isoPath, true, update); - Answer createIsoAnswer = endpoint.sendMessage(configDriveIsoCommand); - if (!createIsoAnswer.getResult()) { - throw new ResourceUnavailableException(String.format("%s ISO failed, details: %s", - (update?"Update":"Create"), createIsoAnswer.getDetails()), ConfigDriveNetworkElement.class, 0L); + + if (agentId == null || dataStore == null) { + throw new ResourceUnavailableException("Config drive iso creation failed, agent or datastore not available", + ConfigDriveNetworkElement.class, 0L); } - configureConfigDriveDisk(profile, secondaryStore); - // Re-attach the ISO if the machine is running - if (update && profile.getVirtualMachine().getState().equals(VirtualMachine.State.Running)) { - s_logger.debug("Re-attach config drive ISO for vm " + profile.getInstanceName() + " in host " + _hostDao.findById(hostId)); - attachIso(secondaryStore, profile.getInstanceName(), hostId, deviceKey); + LOG.debug("Deleting config drive ISO for vm: " + vm.getInstanceName()); + + final String isoPath = ConfigDrive.createConfigDrivePath(vm.getInstanceName()); + final HandleConfigDriveIsoCommand configDriveIsoCommand = new HandleConfigDriveIsoCommand(isoPath, null, dataStore.getTO(), false); + + final Answer answer = agentManager.easySend(agentId, configDriveIsoCommand); + if (!answer.getResult()) { + LOG.error("Failed to remove config drive for instance: " + vm.getInstanceName()); + return false; } return true; - } - private void configureConfigDriveDisk(VirtualMachineProfile profile, DataStore secondaryStore) { + private void addConfigDriveDisk(final VirtualMachineProfile profile, final DataStore dataStore) { boolean isoAvailable = false; - String isoPath = CONFIGDRIVEDIR + "/" + profile.getInstanceName() + "/" + CONFIGDRIVEFILENAME; + final String isoPath = ConfigDrive.createConfigDrivePath(profile.getInstanceName()); for (DiskTO dataTo : profile.getDisks()) { if (dataTo.getPath().equals(isoPath)) { isoAvailable = true; @@ -361,71 +400,34 @@ private void configureConfigDriveDisk(VirtualMachineProfile profile, DataStore s } if (!isoAvailable) { TemplateObjectTO dataTO = new TemplateObjectTO(); - dataTO.setDataStore(secondaryStore.getTO()); + dataTO.setDataStore(dataStore.getTO()); dataTO.setUuid(profile.getUuid()); dataTO.setPath(isoPath); dataTO.setFormat(Storage.ImageFormat.ISO); profile.addDisk(new DiskTO(dataTO, CONFIGDRIVEDISKSEQ.longValue(), isoPath, Volume.Type.ISO)); + } else { + LOG.warn("Config drive iso already is in VM profile."); } } - private boolean updateConfigDrive(VirtualMachineProfile profile, String publicKey, NicProfile nic) { - UserVmVO vm = _userVmDao.findById(profile.getId()); + private boolean configureConfigDriveData(final VirtualMachineProfile profile, final NicProfile nic) { + final UserVmVO vm = _userVmDao.findById(profile.getId()); if (vm.getType() != VirtualMachine.Type.User) { return false; } - // add/update userdata and/or password info into vm profile - Nic defaultNic = _networkModel.getDefaultNic(vm.getId()); + final Nic defaultNic = _networkModel.getDefaultNic(vm.getId()); if (defaultNic != null) { + final String sshPublicKey = getSshKey(profile); final String serviceOffering = _serviceOfferingDao.findByIdIncludingRemoved(vm.getId(), vm.getServiceOfferingId()).getDisplayText(); boolean isWindows = _guestOSCategoryDao.findById(_guestOSDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows"); - List<String[]> vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), - vm.getUuid(), nic.getIPv4Address(), publicKey, (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows); + final List<String[]> vmData = _networkModel.generateVmData(vm.getUserData(), serviceOffering, vm.getDataCenterId(), vm.getInstanceName(), vm.getHostName(), vm.getId(), + vm.getUuid(), nic.getIPv4Address(), sshPublicKey, (String) profile.getParameter(VirtualMachineProfile.Param.VmPassword), isWindows); profile.setVmData(vmData); profile.setConfigDriveLabel(VirtualMachineManager.VmConfigDriveLabel.value()); } return true; } - private Integer detachIso (DataStore secondaryStore, String instanceName, Long hostId) throws ResourceUnavailableException { - String isoPath = CONFIGDRIVEDIR + "/" + instanceName + "/" + CONFIGDRIVEFILENAME; - AttachIsoCommand isoCommand = new AttachIsoCommand(instanceName, secondaryStore.getUri() + "/" + isoPath, false, CONFIGDRIVEDISKSEQ, true); - isoCommand.setStoreUrl(secondaryStore.getUri()); - Answer attachIsoAnswer = null; - - try { - attachIsoAnswer = _agentManager.send(hostId, isoCommand); - } catch (OperationTimedoutException e) { - throw new ResourceUnavailableException("Detach ISO failed: " + e.getMessage(), ConfigDriveNetworkElement.class, 0L); - } - - if (!attachIsoAnswer.getResult()) { - throw new ResourceUnavailableException("Detach ISO failed: " + attachIsoAnswer.getDetails(), ConfigDriveNetworkElement.class, 0L); - } - - if (attachIsoAnswer instanceof AttachIsoAnswer) { - return ((AttachIsoAnswer)attachIsoAnswer).getDeviceKey(); - } else { - return CONFIGDRIVEDISKSEQ; - } - } - - private void attachIso (DataStore secondaryStore, String instanceName, Long hostId, Integer deviceKey) throws ResourceUnavailableException { - String isoPath = CONFIGDRIVEDIR + "/" + instanceName + "/" + CONFIGDRIVEFILENAME; - AttachIsoCommand isoCommand = new AttachIsoCommand(instanceName, secondaryStore.getUri() + "/" + isoPath, true); - isoCommand.setStoreUrl(secondaryStore.getUri()); - isoCommand.setDeviceKey(deviceKey); - Answer attachIsoAnswer = null; - try { - attachIsoAnswer = _agentManager.send(hostId, isoCommand); - } catch (OperationTimedoutException e) { - throw new ResourceUnavailableException("Attach ISO failed: " + e.getMessage() ,ConfigDriveNetworkElement.class,0L); - } - if (!attachIsoAnswer.getResult()) { - throw new ResourceUnavailableException("Attach ISO failed: " + attachIsoAnswer.getDetails(),ConfigDriveNetworkElement.class,0L); - } - } - } diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index ecdd1125aab..4ac48c9cd31 100644 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -653,10 +653,6 @@ protected void runInContext() { } } - - - - @Override @ActionEvent(eventType = EventTypes.EVENT_VM_RESETPASSWORD, eventDescription = "resetting Vm password", async = true) public UserVm resetVMPassword(ResetVMPasswordCmd cmd, String password) throws ResourceUnavailableException, InsufficientCapacityException { @@ -681,6 +677,11 @@ public UserVm resetVMPassword(ResetVMPasswordCmd cmd, String password) throws Re throw new InvalidParameterValueException("Vm with id " + vmId + " is not in the right state"); } + if (userVm.getState() != State.Stopped) { + s_logger.error("vm is not in the right state: " + vmId); + throw new InvalidParameterValueException("Vm " + userVm + " should be stopped to do password reset"); + } + _accountMgr.checkAccess(caller, null, true, userVm); boolean result = resetVMPasswordInternal(vmId, password); @@ -733,6 +734,14 @@ private boolean resetVMPasswordInternal(Long vmId, String password) throws Resou s_logger.debug("Failed to reset password for the virtual machine; no need to reboot the vm"); return false; } else { + final UserVmVO userVm = _vmDao.findById(vmId); + _vmDao.loadDetails(userVm); + userVm.setPassword(password); + // update the password in vm_details table too + // Check if an SSH key pair was selected for the instance and if so + // use it to encrypt & save the vm password + encryptAndStorePassword(userVm, password); + if (vmInstance.getState() == State.Stopped) { s_logger.debug("Vm " + vmInstance + " is stopped, not rebooting it as a part of password reset"); return true; @@ -796,15 +805,7 @@ public UserVm resetVMSSHKey(ResetVMSSHKeyCmd cmd) throws ResourceUnavailableExce boolean result = resetVMSSHKeyInternal(vmId, sshPublicKey, password); - if (result) { - userVm.setDetail("SSH.PublicKey", sshPublicKey); - if (template != null && template.getEnablePassword()) { - userVm.setPassword(password); - //update the encrypted password in vm_details table too - encryptAndStorePassword(userVm, password); - } - _vmDao.saveDetails(userVm); - } else { + if (!result) { throw new CloudRuntimeException("Failed to reset SSH Key for the virtual machine "); } return userVm; @@ -842,6 +843,16 @@ private boolean resetVMSSHKeyInternal(Long vmId, String sshPublicKey, String pas s_logger.debug("Failed to reset SSH Key for the virtual machine; no need to reboot the vm"); return false; } else { + final UserVmVO userVm = _vmDao.findById(vmId); + _vmDao.loadDetails(userVm); + userVm.setDetail("SSH.PublicKey", sshPublicKey); + if (template.getEnablePassword()) { + userVm.setPassword(password); + //update the encrypted password in vm_details table too + encryptAndStorePassword(userVm, password); + } + _vmDao.saveDetails(userVm); + if (vmInstance.getState() == State.Stopped) { s_logger.debug("Vm " + vmInstance + " is stopped, not rebooting it as a part of SSH Key reset"); return true; @@ -4099,8 +4110,6 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl } } - - _templateMgr.prepareIsoForVmProfile(profile, dest); return true; } @@ -6135,14 +6144,7 @@ public UserVm restoreVMInternal(Account caller, UserVmVO vm, Long newTemplateId) if (template.getEnablePassword()) { password = _mgr.generateRandomPassword(); boolean result = resetVMPasswordInternal(vmId, password); - if (result) { - vm.setPassword(password); - _vmDao.loadDetails(vm); - // update the password in vm_details table too - // Check if an SSH key pair was selected for the instance and if so - // use it to encrypt & save the vm password - encryptAndStorePassword(vm, password); - } else { + if (!result) { throw new CloudRuntimeException("VM reset is completed but failed to reset password for the virtual machine "); } } diff --git a/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java b/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java index ff3f40b2645..e964999ee9d 100644 --- a/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java +++ b/server/test/com/cloud/network/element/ConfigDriveNetworkElementTest.java @@ -16,7 +16,6 @@ // under the License. package com.cloud.network.element; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertFalse; @@ -36,6 +35,12 @@ import java.util.List; import java.util.Map; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; +import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.storage.configdrive.ConfigDrive; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; @@ -43,16 +48,10 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import com.google.common.collect.Maps; - -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; -import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; - +import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.HandleConfigDriveIsoCommand; +import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.dao.DataCenterDao; import com.cloud.deploy.DeployDestination; @@ -89,6 +88,7 @@ import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; +import com.google.common.collect.Maps; public class ConfigDriveNetworkElementTest { @@ -106,6 +106,7 @@ private final long SOID = 31L; private final long HOSTID = NETWORK_ID; + @Mock private DataCenter dataCenter; @Mock private ConfigurationDao _configDao; @Mock private DataCenterDao _dcDao; @Mock private DataStoreManager _dataStoreMgr; @@ -134,6 +135,7 @@ @Mock private ServiceOfferingVO serviceOfferingVO; @Mock private UserVmVO virtualMachine; @Mock private IPAddressVO publicIp; + @Mock private AgentManager agentManager; @InjectMocks private final ConfigDriveNetworkElement _configDrivesNetworkElement = new ConfigDriveNetworkElement(); @InjectMocks @Spy private NetworkModelImpl _networkModel = new NetworkModelImpl(); @@ -145,6 +147,7 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException { _configDrivesNetworkElement._networkModel = _networkModel; when(_dataStoreMgr.getImageStore(DATACENTERID)).thenReturn(dataStore); + when(_ep.select(dataStore)).thenReturn(endpoint); when(_vmDao.findById(VMID)).thenReturn(virtualMachine); when(_dcDao.findById(DATACENTERID)).thenReturn(dataCenterVO); @@ -167,7 +170,9 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException { when(virtualMachine.getInstanceName()).thenReturn(VMINSTANCENAME); when(virtualMachine.getUserData()).thenReturn(VMUSERDATA); when(virtualMachine.getHostName()).thenReturn(VMHOSTNAME); + when(dataCenter.getId()).thenReturn(DATACENTERID); when(deployDestination.getHost()).thenReturn(hostVO); + when(deployDestination.getDataCenter()).thenReturn(dataCenter); when(hostVO.getId()).thenReturn(HOSTID); when(nic.isDefaultNic()).thenReturn(true); when(nic.getNetworkId()).thenReturn(NETWORK_ID); @@ -210,25 +215,23 @@ public void testExpunge() throws NoTransitionException, NoSuchFieldException, Il when(_vmInstanceDao.updateState(VirtualMachine.State.Stopped, VirtualMachine.Event.ExpungeOperation, VirtualMachine.State.Expunging, virtualMachine, null)).thenReturn(true); final Answer answer = mock(Answer.class); - when(endpoint.sendMessage(any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); + when(agentManager.easySend(anyLong(), any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); when(answer.getResult()).thenReturn(true); stateMachine.transitTo(virtualMachine, VirtualMachine.Event.ExpungeOperation, null, _vmInstanceDao); ArgumentCaptor<HandleConfigDriveIsoCommand> commandCaptor = ArgumentCaptor.forClass(HandleConfigDriveIsoCommand.class); - verify(endpoint, times(1)).sendMessage(commandCaptor.capture()); + verify(agentManager, times(1)).easySend(anyLong(), commandCaptor.capture()); HandleConfigDriveIsoCommand deleteCommand = commandCaptor.getValue(); assertThat(deleteCommand.isCreate(), is(false)); - assertThat(deleteCommand.isUpdate(), is(false)); - } @Test public void testRelease() { final Answer answer = mock(Answer.class); - when(endpoint.sendMessage(any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); + when(agentManager.easySend(anyLong(), any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); when(answer.getResult()).thenReturn(true); VirtualMachineProfile profile = new VirtualMachineProfileImpl(virtualMachine, null, serviceOfferingVO, null, null); assertTrue(_configDrivesNetworkElement.release(network, nicp, profile, null)); @@ -240,81 +243,20 @@ public void testGetCapabilities () { } @Test - public void testAddPasswordAndUserdata() throws InsufficientCapacityException, ResourceUnavailableException { - List<String[]> actualVmData = getVmData(); - - assertThat(actualVmData, containsInAnyOrder( - new String[]{"userdata", "user_data", VMUSERDATA}, - new String[]{"metadata", "service-offering", VMOFFERING}, - new String[]{"metadata", "availability-zone", ZONENAME}, - new String[]{"metadata", "local-hostname", VMHOSTNAME}, - new String[]{"metadata", "local-ipv4", "192.168.111.111"}, - new String[]{"metadata", "public-hostname", null}, - new String[]{"metadata", "public-ipv4", "192.168.111.111"}, - new String[]{"metadata", "vm-id", String.valueOf(VMID)}, - new String[]{"metadata", "instance-id", VMINSTANCENAME}, - new String[]{"metadata", "public-keys", PUBLIC_KEY}, - new String[]{"metadata", "cloud-identifier", String.format("CloudStack-{%s}", CLOUD_ID)}, - new String[]{PASSWORD, "vm_password", PASSWORD} - )); - } - - @Test - public void testAddPasswordAndUserdataStaticNat() throws InsufficientCapacityException, ResourceUnavailableException { - when(_ipAddressDao.findByAssociatedVmId(VMID)).thenReturn(publicIp); - when(publicIp.getAddress()).thenReturn(new Ip("7.7.7.7")); - - List<String[]> actualVmData = getVmData(); - - assertThat(actualVmData, containsInAnyOrder( - new String[]{"userdata", "user_data", VMUSERDATA}, - new String[]{"metadata", "service-offering", VMOFFERING}, - new String[]{"metadata", "availability-zone", ZONENAME}, - new String[]{"metadata", "local-hostname", VMHOSTNAME}, - new String[]{"metadata", "local-ipv4", "192.168.111.111"}, - new String[]{"metadata", "public-hostname", "7.7.7.7"}, - new String[]{"metadata", "public-ipv4", "7.7.7.7"}, - new String[]{"metadata", "vm-id", String.valueOf(VMID)}, - new String[]{"metadata", "instance-id", VMINSTANCENAME}, - new String[]{"metadata", "public-keys", PUBLIC_KEY}, - new String[]{"metadata", "cloud-identifier", String.format("CloudStack-{%s}", CLOUD_ID)}, - new String[]{PASSWORD, "vm_password", PASSWORD} - )); - } - - - @Test - public void testAddPasswordAndUserdataUuid() throws InsufficientCapacityException, ResourceUnavailableException { - when(virtualMachine.getUuid()).thenReturn("vm-uuid"); - - List<String[]> actualVmData = getVmData(); - - assertThat(actualVmData, containsInAnyOrder( - new String[]{"userdata", "user_data", VMUSERDATA}, - new String[]{"metadata", "service-offering", VMOFFERING}, - new String[]{"metadata", "availability-zone", ZONENAME}, - new String[]{"metadata", "local-hostname", VMHOSTNAME}, - new String[]{"metadata", "local-ipv4", "192.168.111.111"}, - new String[]{"metadata", "public-hostname", null}, - new String[]{"metadata", "public-ipv4", "192.168.111.111"}, - new String[]{"metadata", "vm-id", "vm-uuid"}, - new String[]{"metadata", "instance-id", "vm-uuid"}, - new String[]{"metadata", "public-keys", PUBLIC_KEY}, - new String[]{"metadata", "cloud-identifier", String.format("CloudStack-{%s}", CLOUD_ID)}, - new String[]{PASSWORD, "vm_password", PASSWORD} - )); - } - - private List<String[]> getVmData() throws InsufficientCapacityException, ResourceUnavailableException { + public void testAddPasswordAndUserData() throws Exception { final Answer answer = mock(Answer.class); final UserVmDetailVO userVmDetailVO = mock(UserVmDetailVO.class); - when(endpoint.sendMessage(any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); + when(agentManager.easySend(anyLong(), any(HandleConfigDriveIsoCommand.class))).thenReturn(answer); when(answer.getResult()).thenReturn(true); when(network.getTrafficType()).thenReturn(Networks.TrafficType.Guest); when(virtualMachine.getState()).thenReturn(VirtualMachine.State.Stopped); + when(virtualMachine.getUuid()).thenReturn("vm-uuid"); when(userVmDetailVO.getValue()).thenReturn(PUBLIC_KEY); when(nicp.getIPv4Address()).thenReturn("192.168.111.111"); when(_userVmDetailsDao.findDetail(anyLong(), anyString())).thenReturn(userVmDetailVO); + when(_ipAddressDao.findByAssociatedVmId(VMID)).thenReturn(publicIp); + when(publicIp.getAddress()).thenReturn(new Ip("7.7.7.7")); + Map<VirtualMachineProfile.Param, Object> parms = Maps.newHashMap(); parms.put(VirtualMachineProfile.Param.VmPassword, PASSWORD); parms.put(VirtualMachineProfile.Param.VmSshPubKey, PUBLIC_KEY); @@ -323,8 +265,11 @@ public void testAddPasswordAndUserdataUuid() throws InsufficientCapacityExceptio network, nicp, profile, deployDestination, null)); ArgumentCaptor<HandleConfigDriveIsoCommand> commandCaptor = ArgumentCaptor.forClass(HandleConfigDriveIsoCommand.class); - verify(endpoint, times(1)).sendMessage(commandCaptor.capture()); - HandleConfigDriveIsoCommand result = commandCaptor.getValue(); - return result.getVmData(); + verify(agentManager, times(1)).easySend(anyLong(), commandCaptor.capture()); + HandleConfigDriveIsoCommand createCommand = commandCaptor.getValue(); + + assertTrue(createCommand.isCreate()); + assertTrue(createCommand.getIsoData().length() > 0); + assertTrue(createCommand.getIsoFile().equals(ConfigDrive.createConfigDrivePath(profile.getInstanceName()))); } } diff --git a/services/secondary-storage/server/pom.xml b/services/secondary-storage/server/pom.xml index 7c7cf9b68bf..87ac1866180 100644 --- a/services/secondary-storage/server/pom.xml +++ b/services/secondary-storage/server/pom.xml @@ -50,6 +50,11 @@ <artifactId>cloud-server</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-engine-storage-configdrive</artifactId> + <version>${project.version}</version> + </dependency> <!-- dependencies for starting a post upload server on ssvm --> <dependency> <groupId>io.netty</groupId> 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 da3ab32e203..125512bba91 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,9 +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; @@ -45,7 +42,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; @@ -56,46 +52,6 @@ import javax.naming.ConfigurationException; -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.http.HttpContentCompressor; -import io.netty.handler.codec.http.HttpRequestDecoder; -import io.netty.handler.codec.http.HttpResponseEncoder; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -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.command.CopyCmdAnswer; import org.apache.cloudstack.storage.command.CopyCommand; @@ -106,6 +62,8 @@ import org.apache.cloudstack.storage.command.UploadStatusAnswer; import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; import org.apache.cloudstack.storage.command.UploadStatusCommand; +import org.apache.cloudstack.storage.configdrive.ConfigDrive; +import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder; import org.apache.cloudstack.storage.template.DownloadManager; import org.apache.cloudstack.storage.template.DownloadManagerImpl; import org.apache.cloudstack.storage.template.DownloadManagerImpl.ZfsPathParser; @@ -117,7 +75,22 @@ import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.cloudstack.utils.security.DigestHelper; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; +import com.amazonaws.services.s3.model.S3ObjectSummary; import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthAnswer; import com.cloud.agent.api.CheckHealthCommand; @@ -164,7 +137,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; @@ -192,6 +164,23 @@ import com.cloud.utils.script.Script; import com.cloud.utils.storage.S3.S3Utils; import com.cloud.vm.SecondaryStorageVm; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; public class NfsSecondaryStorageResource extends ServerResourceBase implements SecondaryStorageResource { @@ -200,8 +189,6 @@ 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<String, String> updatableConfigData = Maps.newHashMap(); static { @@ -338,32 +325,32 @@ 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"); + if (cmd.getIsoData() == null) { + return new Answer(cmd, false, "Invalid config drive ISO data"); + } 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<String[]> 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); + s_logger.debug("config drive iso already exists"); + } + Path tempDir = null; + try { + tempDir = java.nio.file.Files.createTempDirectory(ConfigDrive.CONFIGDRIVEDIR); + File tmpIsoFile = ConfigDriveBuilder.base64StringToFile(cmd.getIsoData(), tempDir.toAbsolutePath().toString(), ConfigDrive.CONFIGDRIVEFILENAME); + copyLocalToNfs(tmpIsoFile, new File(cmd.getIsoFile()), cmd.getDestStore()); + } catch (IOException | ConfigurationException e) { + return new Answer(cmd, false, "Failed due to exception: " + e.getMessage()); + } finally { + try { + if (tempDir != null) { + FileUtils.deleteDirectory(tempDir.toFile()); } + } catch (IOException ioe) { + s_logger.warn("Failed to delete ConfigDrive temporary directory: " + tempDir.toString(), ioe); } } - return createConfigDriveIsoForVM(cmd); + return new Answer(cmd, true, "Successfully saved config drive at secondary storage"); } else { DataStoreTO dstore = cmd.getDestStore(); if (dstore instanceof NfsTO) { @@ -383,237 +370,6 @@ private Answer execute(HandleConfigDriveIsoCommand cmd) { } } - private void updateVmData(List<String[]> 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<String[]> recoverVmData(File isoFile) throws IOException { - String tempDirName = null; - List<String[]> 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; - } - - 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"); ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org > Config drive - only supported for secondary storage > --------------------------------------------------- > > Key: CLOUDSTACK-10290 > URL: https://issues.apache.org/jira/browse/CLOUDSTACK-10290 > Project: CloudStack > Issue Type: Bug > Security Level: Public(Anyone can view this level - this is the > default.) > Affects Versions: 4.11.0.0 > Reporter: Rohit Yadav > Assignee: Daan Hoogland > Priority: Major > > Userdata disk looks like this: > <disk type='file' device='cdrom'> > <driver name='qemu' type='raw' cache='none'/> > <source > file='/mnt/eba12ff3-c3a6-394a-bf0f-23291f1f6266/configdrive.iso'/> > <backingStore/> > <target dev='hdd' bus='ide'/> > <readonly/> > <alias name='ide0-1-1'/> > <address type='drive' controller='0' bus='1' target='0' unit='1'/> > </disk> > Mount is: > root# df /mnt/eba12ff3-c3a6-394a-bf0f-23291f1f6266 > Filesystem 1K-blocks > Used Available Use% Mounted on > some-nfs-server.com:/nfs/secondary/ConfigDrive/i-2-24-VM 66391040 2973696 > 63417344 5% /mnt/eba12ff3-c3a6-394a-bf0f-23291f1f6266 > > issue: where to find a primary storage for a VM that is not yet deployed. The > configdrive is created before a storage is assigned. This order of execution > must be reversed for this to work. -- This message was sent by Atlassian JIRA (v7.6.3#76005)