diff --git a/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java b/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java index 9addd7116ece..120f1875e4a8 100644 --- a/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java +++ b/api/src/main/java/com/cloud/agent/api/to/DataObjectType.java @@ -19,5 +19,5 @@ package com.cloud.agent.api.to; public enum DataObjectType { - VOLUME, SNAPSHOT, TEMPLATE + VOLUME, SNAPSHOT, TEMPLATE, TAR } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 0c1f2b11f327..2fa4f242e84a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -722,6 +722,7 @@ public class ApiConstants { public static final String STDERR = "stderr"; public static final String EXITCODE = "exitcode"; public static final String TARGET_ID = "targetid"; + public static final String FILES = "files"; public static final String VOLUME_IDS = "volumeids"; public enum HostDetails { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java new file mode 100644 index 000000000000..6f4e9c78f3e2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java @@ -0,0 +1,156 @@ +// 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.api.command.admin.diagnostics; + +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiArgValidator; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SystemVmResponse; +import org.apache.cloudstack.api.response.diagnostics.GetDiagnosticsDataResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.diagnostics.DiagnosticsService; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.validator.routines.UrlValidator; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; + +@APICommand(name = GetDiagnosticsDataCmd.APINAME, + responseObject = GetDiagnosticsDataResponse.class, + entityType = {VirtualMachine.class}, + responseHasSensitiveInfo = false, + requestHasSensitiveInfo = false, + description = "Get diagnostics data files from system VMs", + since = "4.12.0.0", + authorized = {RoleType.Admin}) +public class GetDiagnosticsDataCmd extends BaseAsyncCmd { + private static final Logger LOGGER = Logger.getLogger(GetDiagnosticsDataCmd.class); + public static final String APINAME = "getDiagnosticsData"; + @Inject + private DiagnosticsService diagnosticsService; + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.TARGET_ID, + type = BaseCmd.CommandType.UUID, + entityType = SystemVmResponse.class, + required = true, + validations = {ApiArgValidator.PositiveNumber}, + description = "The ID of the system VM instance to retrieve diagnostics data files from") + private Long id; + + @Parameter(name = ApiConstants.FILES, + type = BaseCmd.CommandType.LIST, + collectionType = BaseCmd.CommandType.STRING, + description = "A comma separated list of diagnostics data files to be retrieved. Defaults are taken from global settings if none has been provided.") + private List filesList; + + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + public List getFilesList() { + return filesList; + } + + ///////////////////////////////////////////////////// + /////////////////// Implementation ////////////////// + ///////////////////////////////////////////////////// + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + Account account = CallContext.current().getCallingAccount(); + if (account != null) { + return account.getId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException { + try { + String downloadUrl = diagnosticsService.getDiagnosticsDataCommand(this); + UrlValidator urlValidator = new UrlValidator(); + if (StringUtils.isEmpty(downloadUrl)) { + throw new CloudRuntimeException("Failed to retrieve diagnostics files"); + } + GetDiagnosticsDataResponse response = new GetDiagnosticsDataResponse(); + if (urlValidator.isValid(downloadUrl)){ + response.setUrl(downloadUrl); + response.setObjectName("diagnostics"); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } else { + throw new CloudRuntimeException("failed to generate valid download url: " + downloadUrl); + } + } catch (ServerApiException e) { + throw new CloudRuntimeException("Internal exception caught while retrieving diagnostics files: ", e); + } + } + + @Override + public String getEventType() { + VirtualMachine.Type vmType = _entityMgr.findById(VirtualMachine.class, getId()).getType(); + String eventType = ""; + switch (vmType) { + case ConsoleProxy: + eventType = EventTypes.EVENT_PROXY_DIAGNOSTICS; + break; + case SecondaryStorageVm: + eventType = EventTypes.EVENT_SSVM_DIAGNOSTICS; + break; + case DomainRouter: + eventType = EventTypes.EVENT_ROUTER_DIAGNOSTICS; + break; + } + return eventType; + } + + @Override + public String getEventDescription() { + return "Getting diagnostics data files from system vm: " + this._uuidMgr.getUuid(VirtualMachine.class, getId()); + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.SystemVm; + } + +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/diagnostics/GetDiagnosticsDataResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/diagnostics/GetDiagnosticsDataResponse.java new file mode 100644 index 000000000000..4d6e674b5b3f --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/diagnostics/GetDiagnosticsDataResponse.java @@ -0,0 +1,40 @@ +// 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.api.response.diagnostics; + +import com.cloud.serializer.Param; +import com.cloud.vm.VirtualMachine; +import com.google.gson.annotations.SerializedName; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = VirtualMachine.class) +public class GetDiagnosticsDataResponse extends BaseResponse { + @SerializedName(ApiConstants.URL) + @Param(description = "Storage URL to download retrieve diagnostics data files") + private String url; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java b/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java index a9177af7e0c9..0eae41ddb0fe 100644 --- a/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java +++ b/api/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsService.java @@ -18,12 +18,14 @@ // package org.apache.cloudstack.diagnostics; -import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd; - import java.util.Map; +import org.apache.cloudstack.api.command.admin.diagnostics.GetDiagnosticsDataCmd; +import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd; + public interface DiagnosticsService { Map runDiagnosticsCommand(RunDiagnosticsCmd cmd); + String getDiagnosticsDataCommand(GetDiagnosticsDataCmd getDiagnosticsDataCmd); } \ No newline at end of file diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java index 2c75a78b1a38..b9d6487de561 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VRScripts.java @@ -70,4 +70,6 @@ public class VRScripts { public static final String VR_CFG = "vr_cfg.sh"; public static final String DIAGNOSTICS = "diagnostics.py"; + public static final String RETRIEVE_DIAGNOSTICS = "get_diagnostics_files.py"; + public static final String VR_FILE_CLEANUP = "cleanup.sh"; } \ No newline at end of file diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java index 21372a17f8d4..191a62263f36 100644 --- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java +++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java @@ -23,8 +23,11 @@ import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; +import org.apache.cloudstack.diagnostics.DeleteFileInVrCommand; import org.apache.cloudstack.diagnostics.DiagnosticsAnswer; import org.apache.cloudstack.diagnostics.DiagnosticsCommand; +import org.apache.cloudstack.diagnostics.PrepareFilesAnswer; +import org.apache.cloudstack.diagnostics.PrepareFilesCommand; import org.joda.time.Duration; import java.util.ArrayList; import java.util.HashMap; @@ -196,7 +199,11 @@ private Answer executeQueryCommand(NetworkElementCommand cmd) { } else if (cmd instanceof GetRouterAlertsCommand) { return execute((GetRouterAlertsCommand)cmd); } else if (cmd instanceof DiagnosticsCommand) { - return execute((DiagnosticsCommand)cmd); + return execute((DiagnosticsCommand) cmd); + } else if (cmd instanceof PrepareFilesCommand) { + return execute((PrepareFilesCommand) cmd); + } else if (cmd instanceof DeleteFileInVrCommand) { + return execute((DeleteFileInVrCommand)cmd); } else { s_logger.error("Unknown query command in VirtualRoutingResource!"); return Answer.createUnsupportedCommandAnswer(cmd); @@ -306,6 +313,24 @@ private Answer execute(DiagnosticsCommand cmd) { return new DiagnosticsAnswer(cmd, result.isSuccess(), result.getDetails()); } + private Answer execute(PrepareFilesCommand cmd) { + String fileList = String.join(" ", cmd.getFilesToRetrieveList()); + _eachTimeout = Duration.standardSeconds(cmd.getTimeout()); + final ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.RETRIEVE_DIAGNOSTICS, fileList, _eachTimeout); + if (result.isSuccess()) { + return new PrepareFilesAnswer(cmd, true, result.getDetails()); + } + return new PrepareFilesAnswer(cmd, false, result.getDetails()); + } + + private Answer execute(DeleteFileInVrCommand cmd) { + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.VR_FILE_CLEANUP, cmd.getFileName()); + if (result.isSuccess()) { + return new Answer(cmd, result.isSuccess(), result.getDetails()); + } + return new Answer(cmd, result.isSuccess(), result.getDetails()); + } + private Answer execute(GetDomRVersionCmd cmd) { final ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), VRScripts.VERSION, null); if (!result.isSuccess()) { diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageAnswer.java b/core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageAnswer.java new file mode 100644 index 000000000000..044eccbbc979 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageAnswer.java @@ -0,0 +1,26 @@ +// 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.diagnostics; + +import com.cloud.agent.api.Answer; + +public class CopyToSecondaryStorageAnswer extends Answer { + + public CopyToSecondaryStorageAnswer(CopyToSecondaryStorageCommand command, boolean success, String details) { + super(command, success, details); + } +} diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageCommand.java b/core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageCommand.java new file mode 100644 index 000000000000..8e76aad580f7 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageCommand.java @@ -0,0 +1,53 @@ +// 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.diagnostics; + +import org.apache.cloudstack.storage.command.StorageSubSystemCommand; + +public class CopyToSecondaryStorageCommand extends StorageSubSystemCommand { + private String secondaryStorageUrl; + private String systemVmIp; + private String fileName; + + public CopyToSecondaryStorageCommand(String secondaryStorageUrl, String systemVmIp, String fileName) { + this.secondaryStorageUrl = secondaryStorageUrl; + this.systemVmIp = systemVmIp; + this.fileName = fileName; + } + + public String getSecondaryStorageUrl() { + return secondaryStorageUrl; + } + + public String getSystemVmIp() { + return systemVmIp; + } + + public String getFileName() { + return fileName; + } + + @Override + public boolean executeInSequence() { + return false; + } + + @Override + public void setExecuteInSequence(boolean inSeq) { + + } +} diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/DeleteFileInVrCommand.java b/core/src/main/java/org/apache/cloudstack/diagnostics/DeleteFileInVrCommand.java new file mode 100644 index 000000000000..025168b6f093 --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/diagnostics/DeleteFileInVrCommand.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.diagnostics; + +import com.cloud.agent.api.routing.NetworkElementCommand; + +public class DeleteFileInVrCommand extends NetworkElementCommand { + private String fileName; + + public DeleteFileInVrCommand(String fileName) { + this.fileName = fileName; + } + + public String getFileName() { + return fileName; + } + + @Override + public boolean isQuery() { + return true; + } +} diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesAnswer.java b/core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesAnswer.java new file mode 100644 index 000000000000..784a84aa8aeb --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesAnswer.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.diagnostics; + +import com.cloud.agent.api.Answer; + +public class PrepareFilesAnswer extends Answer { + + public PrepareFilesAnswer(PrepareFilesCommand command, boolean success, String details) { + super(command, success, details); + } + +} diff --git a/core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesCommand.java b/core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesCommand.java new file mode 100644 index 000000000000..db65544f948e --- /dev/null +++ b/core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesCommand.java @@ -0,0 +1,44 @@ +// 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.diagnostics; + +import java.util.List; + +import com.cloud.agent.api.routing.NetworkElementCommand; + +public class PrepareFilesCommand extends NetworkElementCommand { + private List filesToRetrieveList; + private long timeout; + + public PrepareFilesCommand(List filesToRetrieve, long timeout) { + this.filesToRetrieveList = filesToRetrieve; + this.timeout = timeout; + } + + public List getFilesToRetrieveList() { + return filesToRetrieveList; + } + + public long getTimeout() { + return timeout; + } + + @Override + public boolean isQuery() { + return true; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index c07b2d93435b..2a574043ce3f 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -225,6 +225,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv public static final String SSHKEYSPATH = "/root/.ssh"; public static final String SSHPRVKEYPATH = SSHKEYSPATH + File.separator + "id_rsa.cloud"; public static final String SSHPUBKEYPATH = SSHKEYSPATH + File.separator + "id_rsa.pub.cloud"; + public static final String DEFAULTDOMRSSHPORT = "3922"; public static final String BASH_SCRIPT_PATH = "/bin/bash"; diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyToSecondaryStorageWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyToSecondaryStorageWrapper.java new file mode 100644 index 000000000000..a4e732f9613f --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyToSecondaryStorageWrapper.java @@ -0,0 +1,87 @@ +// 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.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageAnswer; +import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageCommand; +import org.apache.cloudstack.diagnostics.DiagnosticsHelper; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +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.utils.ssh.SshHelper; + +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.setDirFilePermissions; + +@ResourceWrapper(handles = CopyToSecondaryStorageCommand.class) +public class LibvirtCopyToSecondaryStorageWrapper extends CommandWrapper { + public static final Logger LOGGER = Logger.getLogger(LibvirtCopyToSecondaryStorageWrapper.class); + + @Override + public Answer execute(CopyToSecondaryStorageCommand command, LibvirtComputingResource libvirtResource) { + + String diagnosticsZipFile = command.getFileName(); + String vmSshIp = command.getSystemVmIp(); + String secondaryStorageUrl = command.getSecondaryStorageUrl(); + + KVMStoragePoolManager storagePoolMgr = libvirtResource.getStoragePoolMgr(); + KVMStoragePool secondaryPool; + + boolean success; + + secondaryPool = storagePoolMgr.getStoragePoolByURI(secondaryStorageUrl); + String mountPoint = secondaryPool.getLocalPath(); + + // /mnt/SecStorage/uuid/diagnostics_data + String dataDirectoryInSecondaryStore = String.format("%s/%s", mountPoint, DiagnosticsHelper.DIAGNOSTICS_DATA_DIR); + try { + File dataDirectory = new File(dataDirectoryInSecondaryStore); + boolean existsInSecondaryStore = dataDirectory.exists() || dataDirectory.mkdir(); + + // Modify directory file permissions + Path path = Paths.get(dataDirectory.getAbsolutePath()); + setDirFilePermissions(path); + if (existsInSecondaryStore) { + LOGGER.info(String.format("Copying %s from %s to secondary store %s", diagnosticsZipFile, vmSshIp, secondaryStorageUrl)); + int port = Integer.valueOf(LibvirtComputingResource.DEFAULTDOMRSSHPORT); + File permKey = new File(LibvirtComputingResource.SSHPRVKEYPATH); + SshHelper.scpFrom(vmSshIp, port, "root", permKey, dataDirectoryInSecondaryStore, diagnosticsZipFile); + } + // Verify File copy to Secondary Storage + File fileInSecondaryStore = new File(dataDirectoryInSecondaryStore + diagnosticsZipFile.replace("/root", "")); + if (fileInSecondaryStore.exists()) { + return new CopyToSecondaryStorageAnswer(command, true, "File copied to secondary storage successfully"); + } else { + return new CopyToSecondaryStorageAnswer(command, false, "Zip file " + diagnosticsZipFile.replace("/root/", "") + "not found in secondary storage"); + } + + } catch (Exception e) { + return new CopyToSecondaryStorageAnswer(command, false, e.getMessage()); + } finally { + // unmount secondary storage from hypervisor host + secondaryPool.delete(); + } + } +} diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java index e1066b755e3b..80af6a18a1e6 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java @@ -29,6 +29,8 @@ import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -49,6 +51,9 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageAnswer; +import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageCommand; +import org.apache.cloudstack.diagnostics.DiagnosticsHelper; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.collections.CollectionUtils; @@ -159,6 +164,8 @@ import com.xensource.xenapi.VM; import com.xensource.xenapi.XenAPIObject; +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.setDirFilePermissions; + /** * CitrixResourceBase encapsulates the calls to the XenServer Xapi process to * perform the required functionalities for CloudStack. @@ -5611,4 +5618,74 @@ public boolean attachConfigDriveToMigratedVm(Connection conn, String vmName, Str } + /** + * Get Diagnostics Data API + * Copy zip file from system vm and copy file directly to secondary storage + */ + public Answer copyDiagnosticsFileToSecondaryStorage(Connection conn, CopyToSecondaryStorageCommand cmd) { + String secondaryStorageUrl = cmd.getSecondaryStorageUrl(); + String vmIP = cmd.getSystemVmIp(); + String diagnosticsZipFile = cmd.getFileName(); + + String localDir = null; + boolean success; + + // Mount Secondary storage + String secondaryStorageMountPath = null; + try { + URI uri = new URI(secondaryStorageUrl); + secondaryStorageMountPath = uri.getHost() + ":" + uri.getPath(); + localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(secondaryStorageMountPath.getBytes()); + String mountPoint = mountNfs(conn, secondaryStorageMountPath, localDir); + if (org.apache.commons.lang.StringUtils.isBlank(mountPoint)) { + return new CopyToSecondaryStorageAnswer(cmd, false, "Could not mount secondary storage " + secondaryStorageMountPath + " on host " + localDir); + } + + String dataDirectoryInSecondaryStore = localDir + "/" + DiagnosticsHelper.DIAGNOSTICS_DATA_DIR; + File dataDirectory = new File(dataDirectoryInSecondaryStore); + boolean existsInSecondaryStore = dataDirectory.exists() || dataDirectory.mkdir(); + + // Modify directory file permissions + Path path = Paths.get(dataDirectory.getAbsolutePath()); + setDirFilePermissions(path); + + if (existsInSecondaryStore) { + int port = 3922; + File permKey = new File("/root/.ssh/id_rsa.cloud"); + SshHelper.scpFrom(vmIP, port, "root", permKey, dataDirectoryInSecondaryStore, diagnosticsZipFile); + } + + File fileInSecondaryStore = new File(dataDirectoryInSecondaryStore + diagnosticsZipFile.replace("/root", "")); + if (fileInSecondaryStore.exists()) { + return new CopyToSecondaryStorageAnswer(cmd, true, "File copied to secondary storage successfully."); + } else { + return new CopyToSecondaryStorageAnswer(cmd, false, "Zip file " + diagnosticsZipFile.replace("/root/", "") + "not found in secondary storage"); + } + } catch (Exception e) { + String msg = String.format("Exception caught zip file copy to secondary storage URI: " + secondaryStorageUrl, e); + s_logger.error(msg); + return new CopyToSecondaryStorageAnswer(cmd, false, msg); + } finally { + if (localDir != null) umountNfs(conn, secondaryStorageMountPath, localDir); + } + } + + private String mountNfs(Connection conn, String remoteDir, String localDir) { + if (localDir == null) { + localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(remoteDir.getBytes()); + } + return callHostPlugin(conn, "cloud-plugin-storage", "mountNfsSecondaryStorage", "localDir", localDir, "remoteDir", remoteDir); + } + + // Unmount secondary storage from host + private void umountNfs(Connection conn, String remoteDir, String localDir) { + if (localDir == null) { + localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(remoteDir.getBytes()); + } + String result = callHostPlugin(conn, "cloud-plugin-storage", "umountNfsSecondaryStorage", "localDir", localDir, "remoteDir", remoteDir); + if (org.apache.commons.lang.StringUtils.isBlank(result)) { + String errMsg = "Could not umount secondary storage " + remoteDir + " on host " + localDir; + s_logger.warn(errMsg); + } + } } diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCoppyToSecondaryStorageCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCoppyToSecondaryStorageCommandWrapper.java new file mode 100644 index 000000000000..cacab0f75a69 --- /dev/null +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCoppyToSecondaryStorageCommandWrapper.java @@ -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. +// +package com.cloud.hypervisor.xenserver.resource.wrapper.xenbase; + +import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageCommand; +import org.apache.log4j.Logger; + +import com.cloud.agent.api.Answer; +import com.cloud.hypervisor.xenserver.resource.CitrixResourceBase; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.xensource.xenapi.Connection; + + +@ResourceWrapper(handles = CopyToSecondaryStorageCommand.class) +public class CitrixCoppyToSecondaryStorageCommandWrapper extends CommandWrapper { + public static final Logger LOGGER = Logger.getLogger(CitrixCoppyToSecondaryStorageCommandWrapper.class); + + @Override + public Answer execute(CopyToSecondaryStorageCommand cmd, CitrixResourceBase citrixResourceBase) { + final Connection conn = citrixResourceBase.getConnection(); + String msg = String.format("Copying diagnostics zip file %s from system vm %s to secondary storage %s", cmd.getFileName(), cmd.getSystemVmIp(), cmd.getSecondaryStorageUrl()); + LOGGER.debug(msg); + // Allow the hypervisor host to copy file from system VM to mounted secondary storage + return citrixResourceBase.copyDiagnosticsFileToSecondaryStorage(conn, cmd); + } +} \ No newline at end of file diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index 8e2bc7e12a85..449a943fe73b 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -1364,6 +1364,20 @@ public boolean imageStoreHasEnoughCapacity(DataStore imageStore) { return false; } + /** + * Calculates secondary storage disk capacity against a configurable threshold instead of the hardcoded default 95 % value + * @param imageStore secondary storage + * @param storeCapThreshold the threshold capacity for computing if secondary storage has enough space to accommodate the @this object + * @return + */ + public boolean imageStoreHasEnoughCapacity(DataStore imageStore, Double storeCapThreshold) { + StorageStats imageStoreStats = _storageStats.get(imageStore.getId()); + if (imageStoreStats != null && (imageStoreStats.getByteUsed() / (imageStoreStats.getCapacityBytes() * 1.0)) <= storeCapThreshold) { + return true; + } + return false; + } + /** * Sends VMs metrics to the configured graphite host. */ diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsHelper.java b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsHelper.java new file mode 100644 index 000000000000..b7295ba909d7 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsHelper.java @@ -0,0 +1,82 @@ +// +// 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.diagnostics; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.script.Script2; + +public class DiagnosticsHelper { + private static final Logger LOGGER = Logger.getLogger(DiagnosticsHelper.class); + + public static final String DIAGNOSTICS_DATA_DIR = "diagnostics_data"; + + public static void setDirFilePermissions(Path path) throws java.io.IOException { + Set perms = Files.readAttributes(path, PosixFileAttributes.class).permissions(); + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_EXECUTE); + perms.add(PosixFilePermission.GROUP_WRITE); + perms.add(PosixFilePermission.GROUP_READ); + perms.add(PosixFilePermission.GROUP_EXECUTE); + perms.add(PosixFilePermission.OTHERS_WRITE); + perms.add(PosixFilePermission.OTHERS_READ); + perms.add(PosixFilePermission.OTHERS_EXECUTE); + Files.setPosixFilePermissions(path, perms); + } + + public static void umountSecondaryStorage(String mountPoint) { + if (StringUtils.isNotBlank(mountPoint)) { + Script2 umountCmd = new Script2("/bin/bash", LOGGER); + umountCmd.add("-c"); + String cmdLine = String.format("umount %s", mountPoint); + umountCmd.add(cmdLine); + umountCmd.execute(); + } + } + + public static Long getFileCreationTime(File file) throws IOException { + Path p = Paths.get(file.getAbsolutePath()); + BasicFileAttributes view = Files.getFileAttributeView(p, BasicFileAttributeView.class).readAttributes(); + FileTime fileTime = view.creationTime(); + return fileTime.toMillis(); + } + + public static Long getTimeDifference(File f) { + Long fileCreationTime = null; + try { + fileCreationTime = getFileCreationTime(f); + } catch (IOException e) { + LOGGER.error("File not found: " + e); + } + return (fileCreationTime != null) ? (System.currentTimeMillis() - fileCreationTime) / 1000 : 1L; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java index 21bb0a1889e5..fe7f18a758f2 100644 --- a/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImpl.java @@ -17,33 +17,71 @@ // under the License. package org.apache.cloudstack.diagnostics; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.api.command.admin.diagnostics.GetDiagnosticsDataCmd; +import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd; +import org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesList; +import org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesListFactory; +import org.apache.cloudstack.diagnostics.to.DiagnosticsDataObject; +import org.apache.cloudstack.diagnostics.to.DiagnosticsDataTO; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +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.ZoneScope; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.poll.BackgroundPollManager; +import org.apache.cloudstack.poll.BackgroundPollTask; +import org.apache.cloudstack.storage.NfsMountManager; +import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.agent.api.to.DataTO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.DataCenterDao; import com.cloud.event.ActionEvent; import com.cloud.event.EventTypes; import com.cloud.exception.InvalidParameterValueException; import com.cloud.hypervisor.Hypervisor; +import com.cloud.server.StatsCollector; +import com.cloud.storage.ImageStoreDetailsUtil; +import com.cloud.storage.Storage; +import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.component.PluggableService; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.ssh.SshHelper; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.VMInstanceDao; import com.google.common.base.Strings; -import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.log4j.Logger; -public class DiagnosticsServiceImpl extends ManagerBase implements PluggableService, DiagnosticsService { +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.getTimeDifference; +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.setDirFilePermissions; +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.umountSecondaryStorage; +import static org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesList.CpvmDefaultSupportedFiles; +import static org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesList.SsvmDefaultSupportedFiles; +import static org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesList.VrDefaultSupportedFiles; + +public class DiagnosticsServiceImpl extends ManagerBase implements PluggableService, DiagnosticsService, Configurable { private static final Logger LOGGER = Logger.getLogger(DiagnosticsServiceImpl.class); @Inject @@ -54,6 +92,34 @@ public class DiagnosticsServiceImpl extends ManagerBase implements PluggableServ private VirtualMachineManager vmManager; @Inject private NetworkOrchestrationService networkManager; + @Inject + private StatsCollector statsCollector; + @Inject + private DataStoreManager storeMgr; + @Inject + private BackgroundPollManager backgroundPollManager; + @Inject + private ImageStoreDetailsUtil imageStoreDetailsUtil; + @Inject + private NfsMountManager mountManager; + @Inject + private DataCenterDao dataCenterDao; + + // This 2 settings should require a restart of the management server? + private static final ConfigKey EnableGarbageCollector = new ConfigKey<>("Advanced", Boolean.class, + "diagnostics.data.gc.enable", "true", "enable the diagnostics data files garbage collector", true); + private static final ConfigKey GarbageCollectionInterval = new ConfigKey<>("Advanced", Integer.class, + "diagnostics.data.gc.interval", "86400", "garbage collection interval in seconds", true); + + // These are easily computed properties and need not need a restart of the management server + private static final ConfigKey DataRetrievalTimeout = new ConfigKey<>("Advanced", Long.class, + "diagnostics.data.retrieval.timeout", "3600", "overall data retrieval timeout in seconds", true); + private static final ConfigKey MaximumFileAgeforGarbageCollection = new ConfigKey<>("Advanced", Long.class, + "diagnostics.data.max.file.age", "86400", "maximum file age for garbage collection in seconds", true); + private static final ConfigKey DiskQuotaPercentageThreshold = new ConfigKey<>("Advanced", Double.class, + "diagnostics.data.disable.threshold", "0.95", "Minimum disk space percentage to initiate diagnostics file retrieval", true); + + private final static String DIAGNOSTICS_DATA_DIRECTORY = "diagnostics_data"; @Override @ActionEvent(eventType = EventTypes.EVENT_SYSTEM_VM_DIAGNOSTICS, eventDescription = "running diagnostics on system vm", async = true) @@ -92,13 +158,13 @@ public Map runDiagnosticsCommand(final RunDiagnosticsCmd cmd) { Map detailsMap; - final Answer answer = agentManager.easySend(hostId, command); + Answer answer = agentManager.easySend(hostId, command); - if (answer != null && (answer instanceof DiagnosticsAnswer)) { + if (answer != null) { detailsMap = ((DiagnosticsAnswer) answer).getExecutionDetails(); return detailsMap; } else { - throw new CloudRuntimeException("Failed to execute diagnostics command on remote host: " + answer.getDetails()); + throw new CloudRuntimeException("Failed to execute diagnostics command on remote host: " + vmInstance.getHostName()); } } @@ -110,7 +176,6 @@ protected boolean hasValidChars(String optionalArgs) { final Pattern pattern = Pattern.compile(regex); return pattern.matcher(optionalArgs).find(); } - } protected String prepareShellCmd(String cmdType, String ipAddress, String optionalParams) { @@ -126,10 +191,333 @@ protected String prepareShellCmd(String cmdType, String ipAddress, String option } } + @Override + @ActionEvent(eventType = EventTypes.EVENT_SYSTEM_VM_DIAGNOSTICS, eventDescription = "getting diagnostics files on system vm", async = true) + public String getDiagnosticsDataCommand(GetDiagnosticsDataCmd cmd) { + Long vmId = cmd.getId(); + List optionalFilesList = cmd.getFilesList(); + VMInstanceVO vmInstance = getSystemVMInstance(vmId); + long zoneId = vmInstance.getDataCenterId(); + + List fileList = getFileListToBeRetrieved(optionalFilesList, vmInstance); + + if (CollectionUtils.isEmpty(fileList)) { + throw new CloudRuntimeException("Failed to generate diagnostics file list for retrieval."); + } + + Long vmHostId = vmInstance.getHostId(); + + // Find Secondary Storage with enough Disk Quota in the current Zone + final DataStore store = getImageStore(vmInstance.getDataCenterId()); + + Answer zipFilesAnswer = zipDiagnosticsFilesInSystemVm(vmInstance, fileList); + + if (zipFilesAnswer == null) { + throw new CloudRuntimeException(String.format("Failed to generate diagnostics zip file in VM %s", vmInstance.getUuid())); + } + + if (!zipFilesAnswer.getResult()) { + throw new CloudRuntimeException(String.format("Failed to generate diagnostics zip file file in VM %s due to %s", vmInstance.getUuid(), zipFilesAnswer.getDetails())); + } + + // Copy zip file from system VM to secondary storage + String zipFileInSystemVm = zipFilesAnswer.getDetails().replace("\n", ""); + Pair copyToSecondaryStorageResults = copyZipFileToSecondaryStorage(vmInstance, vmHostId, zipFileInSystemVm, store); + + // Send cleanup zip file cleanup command to system VM + Answer fileCleanupAnswer = deleteDiagnosticsZipFileInsystemVm(vmInstance, zipFileInSystemVm); + if (fileCleanupAnswer == null) { + LOGGER.error(String.format("Failed to cleanup diagnostics zip file on vm: %s", vmInstance.getUuid())); + } else { + if (!fileCleanupAnswer.getResult()) { + LOGGER.error(String.format("Zip file cleanup for vm %s has failed with: %s", vmInstance.getUuid(), fileCleanupAnswer.getDetails())); + } + } + + if (!copyToSecondaryStorageResults.first()) { + throw new CloudRuntimeException(String.format("Failed to copy %s to secondary storage %s due to %s.", zipFileInSystemVm, store.getUri(), copyToSecondaryStorageResults.second())); + } + + // Now we need to create the file download URL + // Find ssvm of store + VMInstanceVO ssvm = getSecondaryStorageVmInZone(zoneId); + if (ssvm == null) { + throw new CloudRuntimeException("No ssvm found in Zone with ID: " + zoneId); + } + // Secondary Storage install path = "diagnostics_data/diagnostics_files_xxxx.tar + String installPath = DIAGNOSTICS_DATA_DIRECTORY + "/" + zipFileInSystemVm.replace("/root", ""); + return createFileDownloadUrl(store, ssvm.getHypervisorType(), installPath); + } + + /** + * Copy retrieved diagnostics zip file from system vm to secondary storage + * For VMware use the mgmt server, and for Xen/KVM use the hyperhost of the target VM + * The strategy is to mount secondary storage on mgmt server or host and scp directly to /mnt/SecStorage/diagnostics_data + * + * @param fileToCopy zip file in system vm to be copied + * @param store secondary storage to copy zip file to + */ + private Pair copyZipFileToSecondaryStorage(VMInstanceVO vmInstance, Long vmHostId, String fileToCopy, DataStore store) { + String vmControlIp = getVMSshIp(vmInstance); + if (StringUtils.isBlank(vmControlIp)) { + return new Pair<>(false, "Unable to find system vm ssh/control IP for vm with ID: " + vmInstance.getId()); + } + Pair copyResult; + if (vmInstance.getHypervisorType() == Hypervisor.HypervisorType.VMware) { + copyResult = orchestrateCopyToSecondaryStorageVMware(store, vmControlIp, fileToCopy); + } else { + copyResult = orchestrateCopyToSecondaryStorageNonVMware(store, vmControlIp, fileToCopy, vmHostId); + } + return copyResult; + } + + private void configureNetworkElementCommand(NetworkElementCommand cmd, VMInstanceVO vmInstance) { + Map accessDetails = networkManager.getSystemVMAccessDetails(vmInstance); + if (StringUtils.isBlank(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + throw new CloudRuntimeException("Unable to set system vm ControlIP for system vm with ID: " + vmInstance.getId()); + } + cmd.setAccessDetail(accessDetails); + } + + private Answer zipDiagnosticsFilesInSystemVm(VMInstanceVO vmInstance, List fileList) { + final PrepareFilesCommand cmd = new PrepareFilesCommand(fileList, DataRetrievalTimeout.value()); + configureNetworkElementCommand(cmd, vmInstance); + Answer answer = agentManager.easySend(vmInstance.getHostId(), cmd); + return answer; + } + + private Answer deleteDiagnosticsZipFileInsystemVm(VMInstanceVO vmInstance, String zipFileName) { + final DeleteFileInVrCommand cmd = new DeleteFileInVrCommand(zipFileName); + configureNetworkElementCommand(cmd, vmInstance); + return agentManager.easySend(vmInstance.getHostId(), cmd); + } + + /** + * Generate a list of diagnostics file to be retrieved depending on the system VM type + * + * @param optionalFileList Optional list of files that user may want to retrieve, empty by default + * @param vmInstance system VM instance, either SSVM, CPVM or VR + * @return a list of files to be retrieved for system VM, either generated from defaults depending on the VM type, or specified + * by the optional list param + */ + private List getFileListToBeRetrieved(List optionalFileList, VMInstanceVO vmInstance) { + DiagnosticsFilesList fileListObject = DiagnosticsFilesListFactory.getDiagnosticsFilesList(optionalFileList, vmInstance); + List fileList = new ArrayList<>(); + + if (fileListObject != null) { + fileList = fileListObject.generateFileList(); + } + return fileList; + } + + private Pair orchestrateCopyToSecondaryStorageNonVMware(final DataStore store, final String vmControlIp, String fileToCopy, Long vmHostId) { + CopyToSecondaryStorageCommand toSecondaryStorageCommand = new CopyToSecondaryStorageCommand(store.getUri(), vmControlIp, fileToCopy); + Answer copyToSecondaryAnswer = agentManager.easySend(vmHostId, toSecondaryStorageCommand); + Pair copyAnswer; + if (copyToSecondaryAnswer != null) { + copyAnswer = new Pair<>(copyToSecondaryAnswer.getResult(), copyToSecondaryAnswer.getDetails()); + } else { + copyAnswer = new Pair<>(false, "Diagnostics Zip file to secondary storage failed"); + } + return copyAnswer; + } + + private Pair orchestrateCopyToSecondaryStorageVMware(final DataStore store, final String vmSshIp, String diagnosticsFile) { + String mountPoint; + boolean success; + + Integer nfsVersion = imageStoreDetailsUtil.getNfsVersion(store.getId()); + mountPoint = mountManager.getMountPoint(store.getUri(), nfsVersion); + if (StringUtils.isNotBlank(mountPoint)) { + LOGGER.info(String.format("Copying %s from %s to secondary store %s", diagnosticsFile, vmSshIp, store.getUri())); + + // dirIn/mnt/SecStorage/uuid/diagnostics_data + String dataDirectoryInSecondaryStore = String.format("%s/%s", mountPoint, DIAGNOSTICS_DATA_DIRECTORY); + try { + File dataDirectory = new File(dataDirectoryInSecondaryStore); + boolean existsInSecondaryStore = dataDirectory.exists() || dataDirectory.mkdir(); + + // Modify directory file permissions + Path path = Paths.get(dataDirectory.getAbsolutePath()); + setDirFilePermissions(path); + + if (existsInSecondaryStore) { + // scp from system VM to mounted sec storage directory + int port = 3922; + File permKey = new File("/var/cloudstack/management/.ssh/id_rsa"); + SshHelper.scpFrom(vmSshIp, port, "root", permKey, dataDirectoryInSecondaryStore, diagnosticsFile); + } + // Verify File copy to Secondary Storage + File fileInSecondaryStore = new File(dataDirectoryInSecondaryStore + diagnosticsFile.replace("/root", "")); + success = fileInSecondaryStore.exists(); + } catch (Exception e) { + String msg = String.format("Exception caught during scp from %s to secondary store %s: ", vmSshIp, dataDirectoryInSecondaryStore); + LOGGER.error(msg); + return new Pair<>(false, msg); + } finally { + // umount secondary storage + umountSecondaryStorage(mountPoint); + } + } else { + return new Pair<>(false, "Failed to mount secondary storage:" + store.getName()); + } + return new Pair<>(success, "File copied to secondary storage successfully"); + } + + // Get ssvm from the zone to use for creating entity download URL + private VMInstanceVO getSecondaryStorageVmInZone(Long zoneId) { + List ssvm = instanceDao.listByZoneIdAndType(zoneId, VirtualMachine.Type.SecondaryStorageVm); + return (CollectionUtils.isEmpty(ssvm)) ? null : ssvm.get(0); + } + + /** + * Iterate through all Image stores in the current running zone and select any that has less than 95% disk usage + * + * @param zoneId of the current running zone + * @return a valid secondary storage with less than DiskQuotaPercentageThreshold set by global config + */ + private DataStore getImageStore(Long zoneId) { + List stores = storeMgr.getImageStoresByScope(new ZoneScope(zoneId)); + if (CollectionUtils.isEmpty(stores)) { + throw new CloudRuntimeException("No Secondary storage found in Zone with Id: " + zoneId); + } + DataStore imageStore = null; + for (DataStore store : stores) { + // Return image store if used percentage is less then threshold value set by global config diagnostics.data.disable.threshold + if (statsCollector.imageStoreHasEnoughCapacity(store, DiskQuotaPercentageThreshold.value())) { + imageStore = store; + break; + } + } + if (imageStore == null) { + throw new CloudRuntimeException("No suitable secondary storage found to retrieve diagnostics in Zone: " + zoneId); + } + return imageStore; + } + + // createEntityExtractUrl throws CloudRuntime exception in case of failure + private String createFileDownloadUrl(DataStore store, Hypervisor.HypervisorType hypervisorType, String filePath) { + // Get image store driver + ImageStoreEntity secStore = (ImageStoreEntity) store; + + //Create dummy TO with hyperType + DataTO dataTO = new DiagnosticsDataTO(hypervisorType, store.getTO()); + DataObject dataObject = new DiagnosticsDataObject(dataTO, store); + return secStore.createEntityExtractUrl(filePath, Storage.ImageFormat.TAR, dataObject); + } + + private VMInstanceVO getSystemVMInstance(Long vmId) { + VMInstanceVO vmInstance = instanceDao.findByIdTypes(vmId, VirtualMachine.Type.ConsoleProxy, + VirtualMachine.Type.DomainRouter, VirtualMachine.Type.SecondaryStorageVm); + if (vmInstance == null) { + String msg = String.format("Unable to find vm instance with id: %s", vmId); + LOGGER.error(msg); + throw new CloudRuntimeException("Diagnostics command execution failed, " + msg); + } + + final Long hostId = vmInstance.getHostId(); + if (hostId == null) { + throw new CloudRuntimeException("Unable to find host for virtual machine instance: " + vmInstance.getInstanceName()); + } + return vmInstance; + } + + private String getVMSshIp(final VMInstanceVO vmInstance) { + Map accessDetails = networkManager.getSystemVMAccessDetails(vmInstance); + String controlIP = accessDetails.get(NetworkElementCommand.ROUTER_IP); + if (StringUtils.isBlank(controlIP)) { + throw new CloudRuntimeException("Unable to find system vm ssh/control IP for vm with ID: " + vmInstance.getId()); + } + return controlIP; + } + + @Override + public boolean start() { + super.start(); + return true; + } + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + if (EnableGarbageCollector.value()) { + backgroundPollManager.submitTask(new GCBackgroundTask(this)); + return true; + } + return false; + } + + public static final class GCBackgroundTask extends ManagedContextRunnable implements BackgroundPollTask { + private DiagnosticsServiceImpl serviceImpl; + + public GCBackgroundTask(DiagnosticsServiceImpl serviceImpl) { + this.serviceImpl = serviceImpl; + } + + private static void deleteOldDiagnosticsFiles(File directory, String storeName) { + File[] fileList = directory.listFiles(); + if (fileList != null) { + String msg = String.format("Found %s diagnostics files in store %s for garbage collection", fileList.length, storeName); + LOGGER.info(msg); + for (File file : fileList) { + if (file.isFile()) { + if (MaximumFileAgeforGarbageCollection.value() <= getTimeDifference(file)) { + boolean success = file.delete(); + LOGGER.info(file.getName() + " delete status: " + success); + } + } + } + } + } + + @Override + protected void runInContext() { + List dcList = serviceImpl.dataCenterDao.listEnabledZones(); + for (DataCenterVO vo: dcList) { + // Get All Image Stores in current running Zone + List storeList = serviceImpl.storeMgr.getImageStoresByScope(new ZoneScope(vo.getId())); + for (DataStore store : storeList) { + String mountPoint = null; + try { + mountPoint = serviceImpl.mountManager.getMountPoint(store.getUri(), null); + if (StringUtils.isNotBlank(mountPoint)) { + File directory = new File(mountPoint + "/" + DIAGNOSTICS_DATA_DIRECTORY); + if (directory.isDirectory()) { + deleteOldDiagnosticsFiles(directory, store.getName()); + } + } + } finally { + // umount secondary storage + umountSecondaryStorage(mountPoint); + } + } + + } + } + + @Override + public Long getDelay() { + // In Milliseconds + return GarbageCollectionInterval.value() * 1000L; + } + } + @Override public List> getCommands() { List> cmdList = new ArrayList<>(); cmdList.add(RunDiagnosticsCmd.class); + cmdList.add(GetDiagnosticsDataCmd.class); return cmdList; } + + @Override + public String getConfigComponentName() { + return DiagnosticsServiceImpl.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{EnableGarbageCollector, DataRetrievalTimeout, + MaximumFileAgeforGarbageCollection, GarbageCollectionInterval, DiskQuotaPercentageThreshold, + SsvmDefaultSupportedFiles, VrDefaultSupportedFiles, CpvmDefaultSupportedFiles}; + } } \ No newline at end of file diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/ConsoleProxyDiagnosticFiles.java b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/ConsoleProxyDiagnosticFiles.java new file mode 100644 index 000000000000..e41edeaa8a82 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/ConsoleProxyDiagnosticFiles.java @@ -0,0 +1,47 @@ +// +// 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.diagnostics.fileprocessor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; + +public class ConsoleProxyDiagnosticFiles implements DiagnosticsFilesList { + // Optional parameters + private List dataTypeList; + + public ConsoleProxyDiagnosticFiles(List dataTypeList) { + this.dataTypeList = dataTypeList; + } + + @Override + public List generateFileList() { + List filesList = new ArrayList<>(); + + if (CollectionUtils.isEmpty(dataTypeList)) { + filesList.addAll(Arrays.asList(CpvmDefaultSupportedFiles.value().split(","))); + + } else { + filesList.addAll(dataTypeList); + } + return filesList; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesList.java b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesList.java new file mode 100644 index 000000000000..e14b3e5d093d --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesList.java @@ -0,0 +1,50 @@ +// 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.diagnostics.fileprocessor; + +import java.util.List; + +import org.apache.cloudstack.framework.config.ConfigKey; + +public interface DiagnosticsFilesList { + + /** + * Global configs below are used to set the diagnostics + * data types applicable for each system vm. + *

+ * the names wrapped in square brackets are for data types that need to first execute a script + * in the system vm and grab output for retrieval, e.g. the output from iptables-save is written to a file + * which will then be retrieved. + */ + ConfigKey SsvmDefaultSupportedFiles = new ConfigKey<>("Advanced", String.class, + "diagnostics.data.ssvm.defaults", "[IPTABLES], [IFCONFIG], [ROUTE], /usr/local/cloud/systemvm/conf/agent.properties," + + " /usr/local/cloud/systemvm/conf/consoleproxy.properties, /var/log/cloud.log", + "List of supported diagnostics data file options for the ssvm", true); + + ConfigKey VrDefaultSupportedFiles = new ConfigKey<>("Advanced", String.class, + "diagnostics.data.vr.defaults", "[IPTABLES], [IFCONFIG], [ROUTE], " + + "/etc/dnsmasq.conf, /etc/resolv.conf, /etc/haproxy.conf, /etc/hosts.conf, /etcdnsmaq-resolv.conf, /var/log/cloud.log, " + + "/var/log/routerServiceMonitor.log, /var/log/dnsmasq.log", + "List of supported diagnostics data file options for the VR", true); + + ConfigKey CpvmDefaultSupportedFiles = new ConfigKey<>("Advanced", String.class, + "diagnostics.data.cpvm.defaults", "[IPTABLES], [IFCONFIG], [ROUTE], /usr/local/cloud/systemvm/conf/agent.properties, " + + "/usr/local/cloud/systemvm/conf/consoleproxy.properties, /var/log/cloud.log", + "List of supported diagnostics data file options for the cpvm", true); + + List generateFileList(); +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesListFactory.java b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesListFactory.java new file mode 100644 index 000000000000..9a4fc3f1d70e --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesListFactory.java @@ -0,0 +1,38 @@ +// 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.diagnostics.fileprocessor; + +import java.util.List; + +import com.cloud.vm.VirtualMachine; + +public class DiagnosticsFilesListFactory { + + public static DiagnosticsFilesList getDiagnosticsFilesList(List dataTypeList, VirtualMachine vm) { + VirtualMachine.Type vmType = vm.getType(); + + if (vmType == VirtualMachine.Type.ConsoleProxy) { + return new ConsoleProxyDiagnosticFiles(dataTypeList); + } else if (vmType == VirtualMachine.Type.SecondaryStorageVm) { + return new SecondaryStorageVmDiagnosticsFiles(dataTypeList); + } else if (vmType == VirtualMachine.Type.DomainRouter) { + return new DomainRouterDiagnosticsFiles(dataTypeList); + } else { + return null; + } + } +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DomainRouterDiagnosticsFiles.java b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DomainRouterDiagnosticsFiles.java new file mode 100644 index 000000000000..80fb8c02e5f6 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DomainRouterDiagnosticsFiles.java @@ -0,0 +1,48 @@ +// +// 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.diagnostics.fileprocessor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; + +public class DomainRouterDiagnosticsFiles implements DiagnosticsFilesList { + // Optional parameters + private List dataTypeList; + + public DomainRouterDiagnosticsFiles(List dataTypeList) { + this.dataTypeList = dataTypeList; + } + + @Override + public List generateFileList() { + List filesList = new ArrayList<>(); + + if (CollectionUtils.isEmpty(dataTypeList)) { + filesList.addAll(Arrays.asList(VrDefaultSupportedFiles.value().split(","))); + + } else { + filesList.addAll(dataTypeList); + } + return filesList; + } + +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SecondaryStorageVmDiagnosticsFiles.java b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SecondaryStorageVmDiagnosticsFiles.java new file mode 100644 index 000000000000..8d321f8d4ce9 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SecondaryStorageVmDiagnosticsFiles.java @@ -0,0 +1,47 @@ +// +// 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.diagnostics.fileprocessor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; + +public class SecondaryStorageVmDiagnosticsFiles implements DiagnosticsFilesList { + // Optional parameters + private List dataTypeList; + + public SecondaryStorageVmDiagnosticsFiles(List dataTypeList) { + this.dataTypeList = dataTypeList; + } + + @Override + public List generateFileList() { + List filesList = new ArrayList<>(); + + if (CollectionUtils.isEmpty(dataTypeList)) { + filesList.addAll(Arrays.asList(SsvmDefaultSupportedFiles.value().split(","))); + + } else { + filesList.addAll(dataTypeList); + } + return filesList; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataObject.java b/server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataObject.java new file mode 100644 index 000000000000..538cdc2a97eb --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataObject.java @@ -0,0 +1,97 @@ +// +// 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.diagnostics.to; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataTO; + +import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; + +public class DiagnosticsDataObject implements DataObject { + private DataTO dataTO; + private DataStore dataStore; + + public DiagnosticsDataObject(DataTO dataTO, DataStore dataStore) { + this.dataTO = dataTO; + this.dataStore = dataStore; + } + + @Override + public long getId() { + return 0; + } + + @Override + public String getUri() { + return null; + } + + @Override + public DataTO getTO() { + return dataTO; + } + + @Override + public DataStore getDataStore() { + return dataStore; + } + + @Override + public Long getSize() { + return null; + } + + @Override + public DataObjectType getType() { + return dataTO.getObjectType(); + } + + @Override + public String getUuid() { + return null; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event) { } + + @Override + public void processEvent(ObjectInDataStoreStateMachine.Event event, Answer answer) { + + } + + @Override + public void incRefCount() { } + + @Override + public void decRefCount() { + + } + + @Override + public Long getRefCount() { + return null; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataTO.java b/server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataTO.java new file mode 100644 index 000000000000..7cadfa553f90 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataTO.java @@ -0,0 +1,60 @@ +// +// 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.diagnostics.to; + +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.DataTO; +import com.cloud.hypervisor.Hypervisor; + +public class DiagnosticsDataTO implements DataTO { + private DataStoreTO dataStoreTO; + private Hypervisor.HypervisorType hypervisorType; + private String path; + private long id; + + public DiagnosticsDataTO(Hypervisor.HypervisorType hypervisorType, DataStoreTO dataStoreTO) { + this.hypervisorType = hypervisorType; + this.dataStoreTO = dataStoreTO; + } + + @Override + public DataObjectType getObjectType() { + return DataObjectType.TAR; + } + + @Override + public DataStoreTO getDataStore() { + return dataStoreTO; + } + + @Override + public Hypervisor.HypervisorType getHypervisorType() { + return hypervisorType; + } + + @Override + public String getPath() { + return path; + } + + @Override + public long getId() { + return id; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/storage/NfsMountManager.java b/server/src/main/java/org/apache/cloudstack/storage/NfsMountManager.java new file mode 100644 index 000000000000..a4e413ced9f0 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/storage/NfsMountManager.java @@ -0,0 +1,23 @@ +// +// 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; + +public interface NfsMountManager { + + String getMountPoint(String storageUrl, Integer nfsVersion); +} diff --git a/server/src/main/java/org/apache/cloudstack/storage/NfsMountManagerImpl.java b/server/src/main/java/org/apache/cloudstack/storage/NfsMountManagerImpl.java new file mode 100644 index 000000000000..50ef13654512 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/storage/NfsMountManagerImpl.java @@ -0,0 +1,203 @@ +// +// 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.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.annotation.PreDestroy; + +import com.cloud.storage.StorageLayer; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.OutputInterpreter; +import com.cloud.utils.script.Script; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +@Component +public class NfsMountManagerImpl implements NfsMountManager { + private static final Logger s_logger = Logger.getLogger(NfsMountManager.class); + + private StorageLayer storage; + private int timeout; + private final Random rand = new Random(System.currentTimeMillis()); + private final ConcurrentMap storageMounts = new ConcurrentHashMap<>(); + + public static final ConfigKey MOUNT_PARENT = new ConfigKey<>("Advanced", String.class, + "mount.parent", "/var/cloudstack/mnt", + "The mount point on the Management Server for Secondary Storage.", + true, ConfigKey.Scope.Global); + + public NfsMountManagerImpl(StorageLayer storage, int timeout) { + this.storage = storage; + this.timeout = timeout; + } + + public String getMountPoint(String storageUrl, Integer nfsVersion) { + String mountPoint = storageMounts.get(storageUrl); + if (mountPoint != null) { + return mountPoint; + } + + URI uri; + try { + uri = new URI(storageUrl); + } catch (URISyntaxException e) { + s_logger.error("Invalid storage URL format ", e); + throw new CloudRuntimeException("Unable to create mount point due to invalid storage URL format " + storageUrl); + } + + mountPoint = mount(uri.getHost() + ":" + uri.getPath(), MOUNT_PARENT.value(), nfsVersion); + if (mountPoint == null) { + s_logger.error("Unable to create mount point for " + storageUrl); + throw new CloudRuntimeException("Unable to create mount point for " + storageUrl); + } + + storageMounts.putIfAbsent(storageUrl, mountPoint); + return mountPoint; + } + + private String mount(String path, String parent, Integer nfsVersion) { + String mountPoint = setupMountPoint(parent); + if (mountPoint == null) { + s_logger.warn("Unable to create a mount point"); + return null; + } + + Script command = new Script(true, "mount", timeout, s_logger); + command.add("-t", "nfs"); + if (nfsVersion != null){ + command.add("-o", "vers=" + nfsVersion); + } + // command.add("-o", "soft,timeo=133,retrans=2147483647,tcp,acdirmax=0,acdirmin=0"); + if ("Mac OS X".equalsIgnoreCase(System.getProperty("os.name"))) { + command.add("-o", "resvport"); + } + command.add(path); + command.add(mountPoint); + String result = command.execute(); + if (result != null) { + s_logger.warn("Unable to mount " + path + " due to " + result); + deleteMountPath(mountPoint); + return null; + } + + // Change permissions for the mountpoint + Script script = new Script(true, "chmod", timeout, s_logger); + script.add("1777", mountPoint); + result = script.execute(); + if (result != null) { + s_logger.warn("Unable to set permissions for " + mountPoint + " due to " + result); + } + return mountPoint; + } + + private String setupMountPoint(String parent) { + String mountPoint = null; + for (int i = 0; i < 10; i++) { + String mntPt = parent + File.separator + String.valueOf(ManagementServerNode.getManagementServerId()) + "." + Integer.toHexString(rand.nextInt(Integer.MAX_VALUE)); + File file = new File(mntPt); + if (!file.exists()) { + if (storage.mkdir(mntPt)) { + mountPoint = mntPt; + break; + } + } + s_logger.error("Unable to create mount: " + mntPt); + } + + return mountPoint; + } + + private void umount(String localRootPath) { + if (!mountExists(localRootPath)) { + return; + } + Script command = new Script(true, "umount", 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); + throw new CloudRuntimeException(errMsg); + } + deleteMountPath(localRootPath); + s_logger.debug("Successfully umounted " + localRootPath); + } + + private void deleteMountPath(String localRootPath) { + try { + Files.deleteIfExists(Paths.get(localRootPath)); + } catch (IOException e) { + s_logger.warn(String.format("unable to delete mount directory %s:%s.%n", localRootPath, e.getMessage())); + } + } + + private boolean mountExists(String localRootPath) { + Script script = new Script(true, "mount", timeout, s_logger); + ZfsPathParser parser = new ZfsPathParser(localRootPath); + script.execute(parser); + return parser.getPaths().stream().filter(s -> s.contains(localRootPath)).findAny().map(s -> true).orElse(false); + } + + public static class ZfsPathParser extends OutputInterpreter { + String _parent; + List paths = new ArrayList<>(); + + public ZfsPathParser(String parent) { + _parent = parent; + } + + @Override + public String interpret(BufferedReader reader) throws IOException { + String line; + while ((line = reader.readLine()) != null) { + paths.add(line); + } + return null; + } + + public List getPaths() { + return paths; + } + + @Override + public boolean drain() { + return true; + } + } + + @PreDestroy + public void destroy() { + s_logger.info("Clean up mounted NFS mount points used in current session."); + storageMounts.values().stream().forEach(this::umount); + } +} diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 2f67c4248d35..f3525cce6b19 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -300,4 +300,12 @@ + + + + + + + + diff --git a/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsFilesListFactoryTest.java b/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsFilesListFactoryTest.java new file mode 100644 index 000000000000..7d39c26d4bbb --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsFilesListFactoryTest.java @@ -0,0 +1,83 @@ +// 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.diagnostics; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; + +import org.apache.cloudstack.diagnostics.fileprocessor.ConsoleProxyDiagnosticFiles; +import org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesListFactory; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class DiagnosticsFilesListFactoryTest { + + private ConsoleProxyDiagnosticFiles proxyDiagnosticFiles; + + @Mock + private VMInstanceVO vmInstance; + + @InjectMocks + private DiagnosticsFilesListFactory listFactory = new DiagnosticsFilesListFactory(); + + @Before + public void setUp() throws Exception { + Mockito.when(vmInstance.getType()).thenReturn(VirtualMachine.Type.ConsoleProxy); + } + + @After + public void tearDown() throws Exception { + Mockito.reset(vmInstance); + } + + @Test + public void testgetDiagnosticsFilesListCpVmDataTypeList() { + List dataTypeList = new ArrayList<>(); + dataTypeList.add("/var/log/auth.log"); + dataTypeList.add("/etc/dnsmasq.conf"); + dataTypeList.add("[IPTABLES]"); + dataTypeList.add("[IFCONFIG]"); + + List files = Objects.requireNonNull(DiagnosticsFilesListFactory.getDiagnosticsFilesList(dataTypeList, vmInstance)).generateFileList(); + + assertEquals(files, dataTypeList); + } + + @Test + public void testDiagnisticsFileListDefaultsCpvm() { + List filesList = Objects.requireNonNull(DiagnosticsFilesListFactory.getDiagnosticsFilesList(null, vmInstance)).generateFileList(); + + ConfigKey configKey = proxyDiagnosticFiles.CpvmDefaultSupportedFiles; + String[] defaultFileArray = configKey.defaultValue().split(","); + + assertEquals(filesList.size(), defaultFileArray.length); + } +} \ No newline at end of file diff --git a/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java b/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java index d85c5434d9a1..04a7e8a2b0d0 100644 --- a/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsServiceImplTest.java @@ -18,15 +18,9 @@ // package org.apache.cloudstack.diagnostics; -import com.cloud.agent.AgentManager; -import com.cloud.agent.api.routing.NetworkElementCommand; -import com.cloud.exception.InvalidParameterValueException; -import com.cloud.utils.exception.CloudRuntimeException; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.VirtualMachine; -import com.cloud.vm.VirtualMachineManager; -import com.cloud.vm.dao.VMInstanceDao; -import junit.framework.TestCase; +import java.util.HashMap; +import java.util.Map; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.diagnostics.RunDiagnosticsCmd; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -39,8 +33,16 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; -import java.util.HashMap; -import java.util.Map; +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.VMInstanceDao; + +import junit.framework.TestCase; @RunWith(MockitoJUnitRunner.class) public class DiagnosticsServiceImplTest extends TestCase { @@ -50,40 +52,39 @@ public class DiagnosticsServiceImplTest extends TestCase { @Mock private VMInstanceDao instanceDao; @Mock - private RunDiagnosticsCmd diagnosticsCmd; + private RunDiagnosticsCmd runDiagnosticsCmd; @Mock private DiagnosticsCommand command; @Mock - private VMInstanceVO instanceVO; + private VMInstanceVO vmInstanceVO; @Mock private VirtualMachineManager vmManager; @Mock private NetworkOrchestrationService networkManager; @InjectMocks - private DiagnosticsServiceImpl diagnosticsService = new DiagnosticsServiceImpl(); + private DiagnosticsServiceImpl serviceImpl = new DiagnosticsServiceImpl(); @Before public void setUp() throws Exception { - Mockito.when(diagnosticsCmd.getId()).thenReturn(1L); - Mockito.when(diagnosticsCmd.getType()).thenReturn(DiagnosticsType.PING); + Mockito.when(runDiagnosticsCmd.getId()).thenReturn(1L); + Mockito.when(runDiagnosticsCmd.getType()).thenReturn(DiagnosticsType.PING); Mockito.when(instanceDao.findByIdTypes(Mockito.anyLong(), Mockito.any(VirtualMachine.Type.class), - Mockito.any(VirtualMachine.Type.class), Mockito.any(VirtualMachine.Type.class))).thenReturn(instanceVO); - + Mockito.any(VirtualMachine.Type.class), Mockito.any(VirtualMachine.Type.class))).thenReturn(vmInstanceVO); } @After public void tearDown() throws Exception { - Mockito.reset(diagnosticsCmd); + Mockito.reset(runDiagnosticsCmd); Mockito.reset(agentManager); Mockito.reset(instanceDao); - Mockito.reset(instanceVO); + Mockito.reset(vmInstanceVO); Mockito.reset(command); } @Test public void testRunDiagnosticsCommandTrue() throws Exception { - Mockito.when(diagnosticsCmd.getAddress()).thenReturn("8.8.8.8"); + Mockito.when(runDiagnosticsCmd.getAddress()).thenReturn("8.8.8.8"); Map accessDetailsMap = new HashMap<>(); accessDetailsMap.put(NetworkElementCommand.ROUTER_IP, "169.20.175.10"); Mockito.when(networkManager.getSystemVMAccessDetails(Mockito.any(VMInstanceVO.class))).thenReturn(accessDetailsMap); @@ -102,7 +103,7 @@ public void testRunDiagnosticsCommandTrue() throws Exception { Mockito.when(agentManager.easySend(Mockito.anyLong(), Mockito.any(DiagnosticsCommand.class))).thenReturn(new DiagnosticsAnswer(command, true, details)); - Map detailsMap = diagnosticsService.runDiagnosticsCommand(diagnosticsCmd); + Map detailsMap = serviceImpl.runDiagnosticsCommand(runDiagnosticsCmd); String stdout = "PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.\n" + "64 bytes from 8.8.8.8: icmp_seq=1 ttl=125 time=7.88 ms\n" + @@ -123,7 +124,7 @@ public void testRunDiagnosticsCommandTrue() throws Exception { @Test public void testRunDiagnosticsCommandFalse() throws Exception { - Mockito.when(diagnosticsCmd.getAddress()).thenReturn("192.0.2.2"); + Mockito.when(runDiagnosticsCmd.getAddress()).thenReturn("192.0.2.2"); Map accessDetailsMap = new HashMap<>(); accessDetailsMap.put(NetworkElementCommand.ROUTER_IP, "169.20.175.10"); @@ -141,7 +142,7 @@ public void testRunDiagnosticsCommandFalse() throws Exception { "4 packets transmitted, 0 packets received, 100% packet loss"; Mockito.when(agentManager.easySend(Mockito.anyLong(), Mockito.any(DiagnosticsCommand.class))).thenReturn(new DiagnosticsAnswer(command, true, details)); - Map detailsMap = diagnosticsService.runDiagnosticsCommand(diagnosticsCmd); + Map detailsMap = serviceImpl.runDiagnosticsCommand(runDiagnosticsCmd); assertEquals(3, detailsMap.size()); assertEquals("Mismatch between actual and expected STDERR", "", detailsMap.get(ApiConstants.STDERR)); @@ -151,46 +152,47 @@ public void testRunDiagnosticsCommandFalse() throws Exception { @Test(expected = InvalidParameterValueException.class) public void testRunDiagnosticsThrowsInvalidParamException() throws Exception { - Mockito.when(diagnosticsCmd.getAddress()).thenReturn(""); + Mockito.when(runDiagnosticsCmd.getAddress()).thenReturn(""); Mockito.when(instanceDao.findByIdTypes(Mockito.anyLong(), Mockito.any(VirtualMachine.Type.class), Mockito.any(VirtualMachine.Type.class), Mockito.any(VirtualMachine.Type.class))).thenReturn(null); - diagnosticsService.runDiagnosticsCommand(diagnosticsCmd); + serviceImpl.runDiagnosticsCommand(runDiagnosticsCmd); } @Test(expected = CloudRuntimeException.class) public void testVMControlIPisNull() throws Exception { - Mockito.when(diagnosticsCmd.getAddress()).thenReturn("0.42.42.42"); + Mockito.when(runDiagnosticsCmd.getAddress()).thenReturn("0.42.42.42"); Map accessDetailsMap = new HashMap<>(); accessDetailsMap.put(NetworkElementCommand.ROUTER_IP, null); Mockito.when(networkManager.getSystemVMAccessDetails(Mockito.any(VMInstanceVO.class))).thenReturn(accessDetailsMap); - diagnosticsService.runDiagnosticsCommand(diagnosticsCmd); + serviceImpl.runDiagnosticsCommand(runDiagnosticsCmd); } @Test public void testInvalidCharsInParams() throws Exception { - assertFalse(diagnosticsService.hasValidChars("'\\''")); - assertFalse(diagnosticsService.hasValidChars("-I eth0 &")); - assertFalse(diagnosticsService.hasValidChars("-I eth0 ;")); - assertFalse(diagnosticsService.hasValidChars(" &2 > ")); - assertFalse(diagnosticsService.hasValidChars(" &2 >> ")); - assertFalse(diagnosticsService.hasValidChars(" | ")); - assertFalse(diagnosticsService.hasValidChars("|")); - assertFalse(diagnosticsService.hasValidChars(",")); + assertFalse(serviceImpl.hasValidChars("'\\''")); + assertFalse(serviceImpl.hasValidChars("-I eth0 &")); + assertFalse(serviceImpl.hasValidChars("-I eth0 ;")); + assertFalse(serviceImpl.hasValidChars(" &2 > ")); + assertFalse(serviceImpl.hasValidChars(" &2 >> ")); + assertFalse(serviceImpl.hasValidChars(" | ")); + assertFalse(serviceImpl.hasValidChars("|")); + assertFalse(serviceImpl.hasValidChars(",")); } @Test public void testValidCharsInParams() throws Exception { - assertTrue(diagnosticsService.hasValidChars("")); - assertTrue(diagnosticsService.hasValidChars(".")); - assertTrue(diagnosticsService.hasValidChars(" ")); - assertTrue(diagnosticsService.hasValidChars("-I eth0 www.google.com")); - assertTrue(diagnosticsService.hasValidChars(" ")); - assertTrue(diagnosticsService.hasValidChars(" -I cloudbr0 --sport ")); - assertTrue(diagnosticsService.hasValidChars(" --back -m20 ")); - assertTrue(diagnosticsService.hasValidChars("-c 5 -4")); - assertTrue(diagnosticsService.hasValidChars("-c 5 -4 -AbDfhqUV")); + assertTrue(serviceImpl.hasValidChars("")); + assertTrue(serviceImpl.hasValidChars(".")); + assertTrue(serviceImpl.hasValidChars(" ")); + assertTrue(serviceImpl.hasValidChars("-I eth0 www.google.com")); + assertTrue(serviceImpl.hasValidChars(" ")); + assertTrue(serviceImpl.hasValidChars(" -I cloudbr0 --sport ")); + assertTrue(serviceImpl.hasValidChars(" --back -m20 ")); + assertTrue(serviceImpl.hasValidChars("-c 5 -4")); + assertTrue(serviceImpl.hasValidChars("-c 5 -4 -AbDfhqUV")); } + } \ No newline at end of file diff --git a/systemvm/debian/opt/cloud/bin/cleanup.sh b/systemvm/debian/opt/cloud/bin/cleanup.sh new file mode 100755 index 000000000000..d14877badbc6 --- /dev/null +++ b/systemvm/debian/opt/cloud/bin/cleanup.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# 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. + +#rm -rf $@ && echo $? + +zip_file=$1 +if [ -e "$zip_file" ]; +then + rm -rf "$zip_file" + echo "Deleting diagnostics zip file $zip_file" +else + echo "File $zip_file not found in vm " +fi diff --git a/systemvm/debian/opt/cloud/bin/get_diagnostics_files.py b/systemvm/debian/opt/cloud/bin/get_diagnostics_files.py new file mode 100755 index 000000000000..3f0b4db5dfcd --- /dev/null +++ b/systemvm/debian/opt/cloud/bin/get_diagnostics_files.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# 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. + +import zipfile +import sys +import time +import re +import subprocess as sp +import shlex +import os +import logging + +fileList = sys.argv[1:] + + +# Create zip archive and append files for retrieval +def zip_files(files): + fList = files + compression = zipfile.ZIP_DEFLATED + time_str = time.strftime("%Y%m%d-%H%M%S") + zf_name = '/root/diagnostics_files_' + time_str + '.tar' + zf = zipfile.ZipFile(zf_name, 'w', compression) + + ''' + Initialize 3 empty arrays to collect found files, non-existent files + and last one to collect temp files to be cleaned up when script exits + ''' + files_found_list = [] + files_not_found_list = [] + files_from_shell_commands = [] + + try: + for f in fList: + # [IPTABLES], [ROUE] and [IFCONFIG], remove square brackets + if '[' in f: + shell_script = re.sub(r'\W+', "", f).strip().lower() + f = execute_shell_script(shell_script) + files_from_shell_commands.append(f) + if os.path.isfile(f): + try: + zf.write(f, f[f.rfind('/') + 1:]) + except OSError or RuntimeError as e: + files_not_found_list.append(f) + else: + files_found_list.append(f) + finally: + cleanup(files_from_shell_commands) + generate_retrieved_files_txt(zf, files_found_list, files_not_found_list) + zf.close() + print zf_name + + +def execute_shell_script(script): + # Ex. iptables.log + outputfile = script + '.log' + + if script == 'iptables': + cmd = 'iptables-save' + elif script == 'ifconfig': + cmd = 'ifconfig' + elif script == 'route': + cmd = 'netstat -rn' + else: + cmd = script + with open(outputfile, 'wb', 0) as f: + try: + p = sp.Popen(shlex.split(cmd), stdout=sp.PIPE, stderr=sp.PIPE) + stdout, stderr = p.communicate() + return_code = p.returncode + if return_code is 0: + f.write(stdout) + else: + f.write(stderr) + except OSError as ex: + delete_tmp_file_cmd = 'rm -f %s' % outputfile + sp.check_call(shlex.split(delete_tmp_file_cmd)) + finally: + f.close() + return outputfile + + +def cleanup(file_list): + files = ' '.join(file_list) + cmd = 'rm -f %s' % files + try: + p = sp.Popen(shlex.split(cmd), stderr=sp.PIPE, stdout=sp.PIPE) + p.communicate() + except OSError as e: + logging.debug("Failed to execute bash command") + + +def generate_retrieved_files_txt(zip_file, files_found, files_not_found): + output_file = 'fileinfo.txt' + try: + with open(output_file, 'wb', 0) as man: + for i in files_found: + man.write(i + '\n') + for j in files_not_found: + man.write(j + 'File Not Found!!\n') + zip_file.write(output_file, output_file) + finally: + cleanup_cmd = "rm -f %s" % output_file + sp.check_call(shlex.split(cleanup_cmd)) + + +if __name__ == '__main__': + zip_files(fileList) diff --git a/test/integration/smoke/test_diagnostics.py b/test/integration/smoke/test_diagnostics.py index 6364d83eeee8..810dbb83093c 100644 --- a/test/integration/smoke/test_diagnostics.py +++ b/test/integration/smoke/test_diagnostics.py @@ -16,11 +16,12 @@ # under the License. """ BVT tests for remote diagnostics of system VMs """ +import urllib + +from marvin.cloudstackAPI import (runDiagnostics, getDiagnosticsData) +from marvin.cloudstackTestCase import cloudstackTestCase # Import Local Modules from marvin.codes import FAILED -from marvin.cloudstackTestCase import cloudstackTestCase -from marvin.cloudstackAPI import runDiagnostics -from marvin.lib.utils import (cleanup_resources) from marvin.lib.base import (Account, ServiceOffering, VirtualMachine) @@ -29,7 +30,7 @@ get_test_template, list_ssvms, list_routers) - +from marvin.lib.utils import (cleanup_resources) from nose.plugins.attrib import attr @@ -537,3 +538,197 @@ def test_12_traceroute_in_cpvm(self): cmd_response.exitcode, 'Failed to run remote Traceroute in CPVM' ) + + ''' + Add Get Diagnostics data BVT + ''' + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true") + def test_13_retrieve_vr_default_files(self): + list_router_response = list_routers( + self.apiclient, + account=self.account.name, + domainid=self.account.domainid + ) + self.assertEqual( + isinstance(list_router_response, list), + True, + "Check list response returns a valid list" + ) + + router = list_router_response[0] + self.debug('Setting up VR with ID %s' % router.id) + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = router.id + + response = self.apiclient.getDiagnosticsData(cmd) + is_valid_url = self.check_url(response.url) + + self.assertEqual( + True, + is_valid_url, + msg="Failed to create valid download url response" + ) + + def check_url(self, url): + import urllib2 + try: + r = urllib.urlopen(url) + if r.code == 200: + return True + except urllib2.HTTPError: + return False + except urllib2.URLError: + return False + return True + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true") + def test_14_retrieve_vr_one_file(self): + list_router_response = list_routers( + self.apiclient, + account=self.account.name, + domainid=self.account.domainid + ) + self.assertEqual( + isinstance(list_router_response, list), + True, + "Check list response returns a valid list" + ) + + router = list_router_response[0] + self.debug('Setting up VR with ID %s' % router.id) + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = router.id + cmd.type = "/var/log/cloud.log" + + response = self.apiclient.getDiagnosticsData(cmd) + + is_valid_url = self.check_url(response.url) + + self.assertEqual( + True, + is_valid_url, + msg="Failed to create valid download url response" + ) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true") + def test_15_retrieve_ssvm_default_files(self): + list_ssvm_response = list_ssvms( + self.apiclient, + systemvmtype='secondarystoragevm', + state='Running', + ) + + self.assertEqual( + isinstance(list_ssvm_response, list), + True, + 'Check list response returns a valid list' + ) + ssvm = list_ssvm_response[0] + + self.debug('Setting up SSVM with ID %s' % ssvm.id) + + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = ssvm.id + + response = self.apiclient.getDiagnosticsData(cmd) + + is_valid_url = self.check_url(response.url) + + self.assertEqual( + True, + is_valid_url, + msg="Failed to create valid download url response" + ) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true") + def test_16_retrieve_ssvm_single_file(self): + list_ssvm_response = list_ssvms( + self.apiclient, + systemvmtype='secondarystoragevm', + state='Running', + ) + + self.assertEqual( + isinstance(list_ssvm_response, list), + True, + 'Check list response returns a valid list' + ) + ssvm = list_ssvm_response[0] + + self.debug('Setting up SSVM with ID %s' % ssvm.id) + + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = ssvm.id + cmd.type = "/var/log/cloud.log" + + response = self.apiclient.getDiagnosticsData(cmd) + + is_valid_url = self.check_url(response.url) + + self.assertEqual( + True, + is_valid_url, + msg="Failed to create valid download url response" + ) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true") + def test_17_retrieve_cpvm_default_files(self): + list_cpvm_response = list_ssvms( + self.apiclient, + systemvmtype='consoleproxy', + state='Running', + ) + + self.assertEqual( + isinstance(list_cpvm_response, list), + True, + 'Check list response returns a valid list' + ) + cpvm = list_cpvm_response[0] + + self.debug('Setting up CPVM with ID %s' % cpvm.id) + + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = cpvm.id + + response = self.apiclient.getDiagnosticsData(cmd) + + is_valid_url = self.check_url(response.url) + + self.assertEqual( + True, + is_valid_url, + msg="Failed to create valid download url response" + ) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="true") + def test_18_retrieve_cpvm_single_file(self): + list_cpvm_response = list_ssvms( + self.apiclient, + systemvmtype='consoleproxy', + state='Running', + ) + + self.assertEqual( + isinstance(list_cpvm_response, list), + True, + 'Check list response returns a valid list' + ) + cpvm = list_cpvm_response[0] + + self.debug('Setting up CPVM with ID %s' % cpvm.id) + + cmd = getDiagnosticsData.getDiagnosticsDataCmd() + cmd.targetid = cpvm.id + cmd.type = "/var/log/cloud.log" + + response = self.apiclient.getDiagnosticsData(cmd) + + is_valid_url = self.check_url(response.url) + + self.assertEqual( + True, + is_valid_url, + msg="Failed to create valid download url response" + ) diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 5294f5658802..771d996eb13b 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -12888,6 +12888,14 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it background-position: -165px -704px; } +.retrieveDiagnostics .icon { + background-position: -35px -125px; +} + +.retrieveDiagnostics:hover .icon { + background-position: -35px -707px; +} + .enableOutOfBandManagement .icon { background-position: -138px -65px; } diff --git a/ui/l10n/en.js b/ui/l10n/en.js index eb7b82250891..2e357b0e5dac 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -242,6 +242,7 @@ var dictionary = { "label.action.force.reconnect.processing":"Reconnecting....", "label.action.generate.keys":"Generate Keys", "label.action.generate.keys.processing":"Generate Keys....", +"label.action.get.diagnostics":"Get Diagnostics Data", "label.action.list.nexusVswitch":"List Nexus 1000v", "label.action.lock.account":"Lock account", "label.action.lock.account.processing":"Locking account....", @@ -792,6 +793,7 @@ var dictionary = { "label.gateway":"Gateway", "label.general.alerts":"General Alerts", "label.generating.url":"Generating URL", +"label.get.diagnostics.files":"Files (comma separated list)", "label.globo.dns":"GloboDNS", "label.globo.dns.configuration":"GloboDNS Configuration", "label.gluster.volume":"Volume", @@ -2126,6 +2128,7 @@ var dictionary = { "message.disabling.network.offering":"Disabling network offering", "message.disabling.vpc.offering":"Disabling VPC offering", "message.disallowed.characters":"Disallowed characters: <,>", +"message.download.diagnostics":"Please click 00000 to download retrieved diagnostics", "message.download.ISO":"Please click 00000 to download ISO", "message.download.template":"Please click 00000 to download template", "message.download.volume":"Please click 00000 to download volume", diff --git a/ui/scripts/system.js b/ui/scripts/system.js index e721581af2dc..9f98f1e061a2 100755 --- a/ui/scripts/system.js +++ b/ui/scripts/system.js @@ -3930,6 +3930,56 @@ } }, + retrieveDiagnostics: { + label: 'label.action.get.diagnostics', + messages: { + notification: function (args) { + return 'label.action.get.diagnostics'; + }, + complete: function(args) { + var url = args.url; + var htmlMsg = _l('message.download.diagnostics'); + var htmlMsg2 = htmlMsg.replace(/#/, url).replace(/00000/, url); + return htmlMsg2; + } + }, + createForm: { + title: 'label.action.get.diagnostics', + desc: '', + fields: { + files: { + label: 'label.get.diagnostics.files' + } + } + }, + action: function (args) { + $.ajax({ + url: createURL("getDiagnosticsData&targetid=" + args.context.routers[0].id + "&files=" + args.data.files), + dataType: "json", + async: true, + success: function(json) { + var jid = json.getdiagnosticsdataresponse.jobid; + args.response.success({ + _custom: { + jobId : jid, + getUpdatedItem: function (json) { + return json.queryasyncjobresultresponse.jobresult.diagnostics; + + }, + getActionFilter: function(){ + return systemvmActionfilter; + } + } + + }); + } + }); //end ajax + }, + notification: { + poll: pollAsyncJobResult + } + }, + viewConsole: { label: 'label.view.console', action: { @@ -9084,6 +9134,56 @@ } }, + retrieveDiagnostics: { + label: 'label.action.get.diagnostics', + messages: { + notification: function (args) { + return 'label.action.get.diagnostics'; + }, + complete: function(args) { + var url = args.url; + var htmlMsg = _l('message.download.diagnostics'); + var htmlMsg2 = htmlMsg.replace(/#/, url).replace(/00000/, url); + return htmlMsg2; + } + }, + createForm: { + title: 'label.action.get.diagnostics', + desc: '', + fields: { + files: { + label: 'label.get.diagnostics.files' + } + } + }, + action: function (args) { + $.ajax({ + url: createURL("getDiagnosticsData&targetid=" + args.context.systemVMs[0].id + "&files=" + args.data.files), + dataType: "json", + async: true, + success: function(json) { + var jid = json.getdiagnosticsdataresponse.jobid; + args.response.success({ + _custom: { + jobId : jid, + getUpdatedItem: function (json) { + return json.queryasyncjobresultresponse.jobresult.diagnostics; + + }, + getActionFilter: function(){ + return systemvmActionfilter; + } + } + + }); + } + }); //end ajax + }, + notification: { + poll: pollAsyncJobResult + } + }, + scaleUp: { label: 'label.change.service.offering', createForm: { @@ -10530,6 +10630,56 @@ } }, + retrieveDiagnostics: { + label: 'label.action.get.diagnostics', + messages: { + notification: function (args) { + return 'label.action.get.diagnostics'; + }, + complete: function(args) { + var url = args.url; + var htmlMsg = _l('message.download.diagnostics'); + var htmlMsg2 = htmlMsg.replace(/#/, url).replace(/00000/, url); + return htmlMsg2; + } + }, + createForm: { + title: 'label.action.get.diagnostics', + desc: '', + fields: { + files: { + label: 'label.get.diagnostics.files' + } + } + }, + action: function (args) { + $.ajax({ + url: createURL("getDiagnosticsData&targetid=" + args.context.routers[0].id + "&files=" + args.data.files), + dataType: "json", + async: true, + success: function(json) { + var jid = json.getdiagnosticsdataresponse.jobid; + args.response.success({ + _custom: { + jobId : jid, + getUpdatedItem: function (json) { + return json.queryasyncjobresultresponse.jobresult.diagnostics; + + }, + getActionFilter: function(){ + return systemvmActionfilter; + } + } + + }); + } + }); //end ajax + }, + notification: { + poll: pollAsyncJobResult + } + }, + scaleUp: { //*** Infrastructure > Virtual Routers > change service offering *** label: 'label.change.service.offering', createForm: { @@ -11880,6 +12030,56 @@ } }, + retrieveDiagnostics: { + label: 'label.action.get.diagnostics', + messages: { + notification: function (args) { + return 'label.action.get.diagnostics'; + }, + complete: function(args) { + var url = args.url; + var htmlMsg = _l('message.download.diagnostics'); + var htmlMsg2 = htmlMsg.replace(/#/, url).replace(/00000/, url); + return htmlMsg2; + } + }, + createForm: { + title: 'label.action.get.diagnostics', + desc: '', + fields: { + files: { + label: 'label.get.diagnostics.files' + } + } + }, + action: function (args) { + $.ajax({ + url: createURL("getDiagnosticsData&targetid=" + args.context.systemVMs[0].id + "&files=" + args.data.files), + dataType: "json", + async: true, + success: function(json) { + var jid = json.getdiagnosticsdataresponse.jobid; + args.response.success({ + _custom: { + jobId : jid, + getUpdatedItem: function (json) { + return json.queryasyncjobresultresponse.jobresult.diagnostics; + + }, + getActionFilter: function(){ + return systemvmActionfilter; + } + } + + }); + } + }); //end ajax + }, + notification: { + poll: pollAsyncJobResult + } + }, + scaleUp: { //*** Infrastructure > System VMs (consoleProxy or SSVM) > change service offering *** label: 'label.change.service.offering', createForm: { @@ -22533,6 +22733,7 @@ if (isAdmin()) { allowedActions.push("migrate"); allowedActions.push("diagnostics"); + allowedActions.push("retrieveDiagnostics"); } } else if (jsonObj.state == 'Starting') { if (isAdmin()) { @@ -22586,6 +22787,7 @@ if (isAdmin()) { allowedActions.push("migrate"); allowedActions.push("diagnostics"); + allowedActions.push("retrieveDiagnostics"); } } else if (jsonObj.state == 'Starting') { if (isAdmin()) { diff --git a/utils/src/main/java/com/cloud/utils/ssh/SshHelper.java b/utils/src/main/java/com/cloud/utils/ssh/SshHelper.java index 88be57742257..042842064df9 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SshHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SshHelper.java @@ -58,6 +58,30 @@ public static void scpTo(String host, int port, String user, File pemKeyFile, St scpTo(host, port, user, pemKeyFile, password, remoteTargetDirectory, data, remoteFileName, fileMode, DEFAULT_CONNECT_TIMEOUT, DEFAULT_KEX_TIMEOUT); } + public static void scpFrom(String host, int port, String user, File permKeyFile, String localTargetDirectory, String remoteTargetFile) throws Exception { + com.trilead.ssh2.Connection conn = null; + com.trilead.ssh2.SCPClient scpClient = null; + + try { + conn = new com.trilead.ssh2.Connection(host, port); + conn.connect(null, DEFAULT_CONNECT_TIMEOUT, DEFAULT_KEX_TIMEOUT); + + if (!conn.authenticateWithPublicKey(user, permKeyFile, null)) { + String msg = "Failed to authentication SSH user " + user + " on host " + host; + s_logger.error(msg); + throw new Exception(msg); + } + scpClient = conn.createSCPClient(); + + scpClient.get(remoteTargetFile, localTargetDirectory); + + } finally { + if (conn != null) { + conn.close(); + } + } + } + public static void scpTo(String host, int port, String user, File pemKeyFile, String password, String remoteTargetDirectory, String localFile, String fileMode, int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {