From be97470d83a0c6e834d97c9860354176016f2c64 Mon Sep 17 00:00:00 2001 From: Paul Angus Date: Wed, 15 Jan 2020 10:38:33 +0000 Subject: [PATCH] Get Diagnostics: Download logs and diagnostics data from SSVM, CPVM, Router (#3350) * * Complete API implementation * Complete UI integration * Complete marvin test * Complete Secondary storage GC background task * improve UI labels * slight reword and add another missing description * improve download message clarity * Address comments * multiple fixes and cleanups Signed-off-by: Rohit Yadav * fix more bugs, let it return ip rule list in another log file Signed-off-by: Rohit Yadav * fix missing iprule bug Signed-off-by: Rohit Yadav * add support for ARCHIVE type of object to be linked/setup on secstorage Signed-off-by: Rohit Yadav * Fix retrieving files for Xenserver * Update get_diagnostics_files.py * Fix bug where executable scripts weren't handled * Fixed error on script cmd generation * Do not filter name for log files as it would override similar prefix script names * Addressed code review comments * log error instead of printstacktrace * Treat script as executable and shell script * Check missing script name case and write to output instead of catching exception * Use shell = true instead of shlex to support any executable * fix xenserver bug * don't set dir permission for vmware * Code review comments - refactoring * Add check for possible NPE * Remove unused imoprt after rebase * Add better description for configs Co-authored-by: Nicolas Vazquez Co-authored-by: Rohit Yadav Co-authored-by: Anurag Awasthi --- .../cloud/agent/api/to/DataObjectType.java | 2 +- .../main/java/com/cloud/storage/Storage.java | 5 +- .../apache/cloudstack/api/ApiConstants.java | 1 + .../diagnostics/GetDiagnosticsDataCmd.java | 157 +++++++ .../GetDiagnosticsDataResponse.java | 40 ++ .../diagnostics/DiagnosticsService.java | 8 +- .../resource/virtualnetwork/VRScripts.java | 2 + .../VirtualRoutingResource.java | 27 +- .../CopyToSecondaryStorageAnswer.java | 26 ++ .../CopyToSecondaryStorageCommand.java | 53 +++ .../diagnostics/DeleteFileInVrCommand.java | 36 ++ .../diagnostics/PrepareFilesAnswer.java | 27 ++ .../diagnostics/PrepareFilesCommand.java | 44 ++ .../resource/LibvirtComputingResource.java | 1 + .../LibvirtCopyToSecondaryStorageWrapper.java | 87 ++++ .../manager/VmwareStorageManagerImpl.java | 2 + .../resource/CitrixResourceBase.java | 67 +++ .../resource/XenServerStorageProcessor.java | 30 +- .../Xenserver625StorageProcessor.java | 10 +- ...CoppyToSecondaryStorageCommandWrapper.java | 43 ++ scripts/vm/hypervisor/xenserver/vmops | 30 +- .../java/com/cloud/server/StatsCollector.java | 14 + .../diagnostics/DiagnosticsHelper.java | 80 ++++ .../diagnostics/DiagnosticsServiceImpl.java | 406 +++++++++++++++++- .../fileprocessor/DiagnosticsFilesList.java | 47 ++ .../DiagnosticsFilesListFactory.java | 36 ++ .../DomainRouterDiagnosticsFiles.java | 52 +++ .../SystemVMDiagnosticsFiles.java | 50 +++ .../diagnostics/to/DiagnosticsDataObject.java | 97 +++++ .../diagnostics/to/DiagnosticsDataTO.java | 60 +++ .../cloudstack/storage/NfsMountManager.java | 23 + .../storage/NfsMountManagerImpl.java | 203 +++++++++ .../spring-server-core-managers-context.xml | 8 + .../DiagnosticsFilesListFactoryTest.java | 83 ++++ .../DiagnosticsServiceImplTest.java | 92 ++-- systemvm/debian/opt/cloud/bin/cleanup.sh | 28 ++ .../opt/cloud/bin/get_diagnostics_files.py | 143 ++++++ test/integration/smoke/test_diagnostics.py | 203 ++++++++- ui/css/cloudstack3.css | 2 + ui/css/src/scss/components/action-icons.scss | 8 + ui/l10n/en.js | 10 +- ui/scripts/system.js | 202 +++++++++ .../java/com/cloud/utils/ssh/SshHelper.java | 24 ++ 43 files changed, 2482 insertions(+), 87 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/diagnostics/GetDiagnosticsDataResponse.java create mode 100644 core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/diagnostics/CopyToSecondaryStorageCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/diagnostics/DeleteFileInVrCommand.java create mode 100644 core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesAnswer.java create mode 100644 core/src/main/java/org/apache/cloudstack/diagnostics/PrepareFilesCommand.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyToSecondaryStorageWrapper.java create mode 100644 plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCoppyToSecondaryStorageCommandWrapper.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsHelper.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesList.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesListFactory.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DomainRouterDiagnosticsFiles.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SystemVMDiagnosticsFiles.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataObject.java create mode 100644 server/src/main/java/org/apache/cloudstack/diagnostics/to/DiagnosticsDataTO.java create mode 100644 server/src/main/java/org/apache/cloudstack/storage/NfsMountManager.java create mode 100644 server/src/main/java/org/apache/cloudstack/storage/NfsMountManagerImpl.java create mode 100644 server/src/test/java/org/apache/cloudstack/diagnostics/DiagnosticsFilesListFactoryTest.java create mode 100755 systemvm/debian/opt/cloud/bin/cleanup.sh create mode 100755 systemvm/debian/opt/cloud/bin/get_diagnostics_files.py 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 9addd7116ec..26294cfbb22 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, ARCHIVE } diff --git a/api/src/main/java/com/cloud/storage/Storage.java b/api/src/main/java/com/cloud/storage/Storage.java index 9093dc34f14..82bc5f6d4e5 100644 --- a/api/src/main/java/com/cloud/storage/Storage.java +++ b/api/src/main/java/com/cloud/storage/Storage.java @@ -16,11 +16,11 @@ // under the License. package com.cloud.storage; -import org.apache.commons.lang.NotImplementedException; - import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang.NotImplementedException; + public class Storage { public static enum ImageFormat { QCOW2(true, true, false, "qcow2"), @@ -33,6 +33,7 @@ public class Storage { VMDK(true, true, false, "vmdk"), VDI(true, true, false, "vdi"), TAR(false, false, false, "tar"), + ZIP(false, false, false, "zip"), DIR(false, false, false, "dir"); private final boolean supportThinProvisioning; 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 e810760314c..44c53f690fb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -742,6 +742,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 00000000000..dc058ff0a28 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/diagnostics/GetDiagnosticsDataCmd.java @@ -0,0 +1,157 @@ +// 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 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 and files from system VMs", + since = "4.14.0.0", + authorized = {RoleType.Admin}) +public class GetDiagnosticsDataCmd extends BaseAsyncCmd { + 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 00000000000..4d6e674b5b3 --- /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 a9177af7e0c..fb1d03b559b 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,16 @@ // 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 { + String DIAGNOSTICS_DIRECTORY = "diagnostics"; + 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 2c75a78b1a3..b9d6487de56 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 21372a17f8d..191a62263f3 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.io.IOException; 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 @@ public class VirtualRoutingResource { } 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 @@ public class VirtualRoutingResource { 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 00000000000..044eccbbc97 --- /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 00000000000..8e76aad580f --- /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 00000000000..025168b6f09 --- /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 00000000000..784a84aa8ae --- /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 00000000000..db65544f948 --- /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 8ee318fa8af..76db243ee6b 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 @@ -226,6 +226,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 00000000000..a6baa1c1785 --- /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 static org.apache.cloudstack.diagnostics.DiagnosticsHelper.setDirFilePermissions; + +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.DiagnosticsService; +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; + +@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, DiagnosticsService.DIAGNOSTICS_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) { + 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/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java index b73d25050c0..97b5088ccae 100644 --- a/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java +++ b/plugins/hypervisors/vmware/src/main/java/com/cloud/hypervisor/vmware/manager/VmwareStorageManagerImpl.java @@ -109,6 +109,8 @@ public class VmwareStorageManagerImpl implements VmwareStorageManager { newPath = createOvaForVolume((VolumeObjectTO)data, timeout); } else if (data.getObjectType() == DataObjectType.TEMPLATE) { newPath = createOvaForTemplate((TemplateObjectTO)data, timeout); + } else if (data.getObjectType() == DataObjectType.ARCHIVE) { + newPath = cmd.getInstallPath(); } if (newPath != null) { cmd.setInstallPath(newPath); 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 4117892b59c..ea168d5275f 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 @@ -49,6 +49,9 @@ import javax.naming.ConfigurationException; 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.DiagnosticsService; import org.apache.cloudstack.hypervisor.xenserver.ExtraConfigurationUtility; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -201,6 +204,7 @@ public abstract class CitrixResourceBase implements ServerResource, HypervisorRe } private final static int BASE_TO_CONVERT_BYTES_INTO_KILOBYTES = 1024; + private final static String BASE_MOUNT_POINT_ON_REMOTE = "/var/cloud_mount/"; private static final XenServerConnectionPool ConnPool = XenServerConnectionPool.getInstance(); // static min values for guests on xenserver @@ -5612,4 +5616,67 @@ public abstract class CitrixResourceBase implements ServerResource, HypervisorRe } + /** + * 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 = BASE_MOUNT_POINT_ON_REMOTE + 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 + File.separator + DiagnosticsService.DIAGNOSTICS_DIRECTORY; + final CopyToSecondaryStorageAnswer answer; + final String scpResult = callHostPlugin(conn, "vmops", "secureCopyToHost", "hostfilepath", dataDirectoryInSecondaryStore, + "srcip", vmIP, "srcfilepath", cmd.getFileName()).toLowerCase(); + + if (scpResult.contains("success")) { + answer = new CopyToSecondaryStorageAnswer(cmd, true, "File copied to secondary storage successfully."); + } else { + answer = new CopyToSecondaryStorageAnswer(cmd, false, "Zip file " + diagnosticsZipFile.replace("/root/", "") + "could not be copied to secondary storage due to " + scpResult); + } + umountNfs(conn, secondaryStorageMountPath, localDir); + localDir = null; + return answer; + } catch (Exception e) { + String msg = "Exception caught zip file copy to secondary storage URI: " + secondaryStorageUrl + "Exception : " + e; + s_logger.error(msg, e); + 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 = BASE_MOUNT_POINT_ON_REMOTE + 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 = BASE_MOUNT_POINT_ON_REMOTE + 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/XenServerStorageProcessor.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java index fc72e79b074..458b7d6377a 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/XenServerStorageProcessor.java @@ -31,21 +31,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.log4j.Logger; -import org.apache.xmlrpc.XmlRpcException; - -import com.google.common.annotations.VisibleForTesting; -import com.xensource.xenapi.Connection; -import com.xensource.xenapi.SR; -import com.xensource.xenapi.Types; -import com.xensource.xenapi.Types.BadServerResponse; -import com.xensource.xenapi.Types.VmPowerState; -import com.xensource.xenapi.Types.XenAPIException; -import com.xensource.xenapi.VBD; -import com.xensource.xenapi.VDI; -import com.xensource.xenapi.VM; - import org.apache.cloudstack.agent.directdownload.DirectDownloadCommand; import org.apache.cloudstack.storage.command.AttachAnswer; import org.apache.cloudstack.storage.command.AttachCommand; @@ -67,6 +52,9 @@ import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.log4j.Logger; +import org.apache.xmlrpc.XmlRpcException; import com.cloud.agent.api.Answer; import com.cloud.agent.api.to.DataObjectType; @@ -86,12 +74,24 @@ import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.resource.StorageProcessor; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.storage.S3.ClientOptions; +import com.google.common.annotations.VisibleForTesting; +import com.xensource.xenapi.Connection; +import com.xensource.xenapi.SR; +import com.xensource.xenapi.Types; +import com.xensource.xenapi.Types.BadServerResponse; +import com.xensource.xenapi.Types.VmPowerState; +import com.xensource.xenapi.Types.XenAPIException; +import com.xensource.xenapi.VBD; +import com.xensource.xenapi.VDI; +import com.xensource.xenapi.VM; public class XenServerStorageProcessor implements StorageProcessor { private static final Logger s_logger = Logger.getLogger(XenServerStorageProcessor.class); protected CitrixResourceBase hypervisorResource; protected String BaseMountPointOnHost = "/var/run/cloud_mount"; + protected final static String BASE_MOUNT_POINT_ON_REMOTE = "/var/cloud_mount/"; + public XenServerStorageProcessor(final CitrixResourceBase resource) { hypervisorResource = resource; } diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/Xenserver625StorageProcessor.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/Xenserver625StorageProcessor.java index ddafc159874..a2c8b708bf3 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/Xenserver625StorageProcessor.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/Xenserver625StorageProcessor.java @@ -71,7 +71,7 @@ public class Xenserver625StorageProcessor extends XenServerStorageProcessor { private void mountNfs(Connection conn, String remoteDir, String localDir) { if (localDir == null) { - localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(remoteDir.getBytes()); + localDir = BASE_MOUNT_POINT_ON_REMOTE + UUID.nameUUIDFromBytes(remoteDir.getBytes()); } String result = hypervisorResource.callHostPluginAsync(conn, "cloud-plugin-storage", "mountNfsSecondaryStorage", 100 * 1000, "localDir", localDir, "remoteDir", remoteDir); if (StringUtils.isBlank(result)) { @@ -241,7 +241,7 @@ public class Xenserver625StorageProcessor extends XenServerStorageProcessor { } protected SR createFileSr(Connection conn, String remotePath, String dir) { - String localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(remotePath.getBytes()); + String localDir = BASE_MOUNT_POINT_ON_REMOTE + UUID.nameUUIDFromBytes(remotePath.getBytes()); mountNfs(conn, remotePath, localDir); return createFileSR(conn, localDir + "/" + dir); } @@ -563,7 +563,7 @@ public class Xenserver625StorageProcessor extends XenServerStorageProcessor { SR snapshotSr = null; Task task = null; try { - final String localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(secondaryStorageMountPath.getBytes()); + final String localDir = BASE_MOUNT_POINT_ON_REMOTE + UUID.nameUUIDFromBytes(secondaryStorageMountPath.getBytes()); mountNfs(conn, secondaryStorageMountPath, localDir); final boolean result = makeDirectory(conn, localDir + "/" + folder); if (!result) { @@ -1074,7 +1074,7 @@ public class Xenserver625StorageProcessor extends XenServerStorageProcessor { srcSr = createFileSr(conn, srcUri.getHost() + ":" + srcUri.getPath(), srcDir); final String destNfsPath = destUri.getHost() + ":" + destUri.getPath(); - final String localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(destNfsPath.getBytes()); + final String localDir = BASE_MOUNT_POINT_ON_REMOTE + UUID.nameUUIDFromBytes(destNfsPath.getBytes()); mountNfs(conn, destUri.getHost() + ":" + destUri.getPath(), localDir); makeDirectory(conn, localDir + "/" + destDir); @@ -1216,7 +1216,7 @@ public class Xenserver625StorageProcessor extends XenServerStorageProcessor { srcSr = hypervisorResource.getIscsiSR(conn, iScsiName, storageHost, iScsiName, chapInitiatorUsername, chapInitiatorSecret, false, srType, true); final String destNfsPath = destUri.getHost() + ":" + destUri.getPath(); - final String localDir = "/var/cloud_mount/" + UUID.nameUUIDFromBytes(destNfsPath.getBytes()); + final String localDir = BASE_MOUNT_POINT_ON_REMOTE + UUID.nameUUIDFromBytes(destNfsPath.getBytes()); mountNfs(conn, destNfsPath, localDir); makeDirectory(conn, localDir + "/" + destDir); 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 00000000000..cacab0f75a6 --- /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/scripts/vm/hypervisor/xenserver/vmops b/scripts/vm/hypervisor/xenserver/vmops index d87edff3882..dd03ded9592 100755 --- a/scripts/vm/hypervisor/xenserver/vmops +++ b/scripts/vm/hypervisor/xenserver/vmops @@ -203,6 +203,33 @@ def createFile(session, args): return txt +@echo +def secureCopyToHost(session, args): + host_filepath = args['hostfilepath'] + src_ip = args['srcip'] + src_filepath = args['srcfilepath'] + src_target = "root@" + src_ip + ":" + src_filepath + # Make any directories as needed + if not os.path.isdir(host_filepath): + try: + os.makedirs(host_filepath) + except OSError, (errno, strerror): + if not os.path.isdir(host_filepath): + errMsg = "OSError while creating " + host_filepath + " with errno: " + str(errno) + " and strerr: " + strerror + logging.debug(errMsg) + return "fail# Cannot create the directory to copy file to " + host_filepath + + # Copy file to created directory + txt="" + try: + txt = util.pread2(['scp','-P','3922','-q','-o','StrictHostKeyChecking=no','-i','/root/.ssh/id_rsa.cloud', src_target, host_filepath]) + util.pread2(['chmod', 'a+r', os.path.join(host_filepath, os.path.basename(src_filepath))]) + txt = 'success#' + txt + except: + logging.error("failed to scp source target " + src_target + " to host at file path " + host_filepath) + txt = 'fail#' + txt + return txt + @echo def createFileInDomr(session, args): src_filepath = args['srcfilepath'] @@ -1560,4 +1587,5 @@ if __name__ == "__main__": "setLinkLocalIP":setLinkLocalIP, "cleanup_rules":cleanup_rules, "createFileInDomr":createFileInDomr, - "kill_copy_process":kill_copy_process}) + "kill_copy_process":kill_copy_process, + "secureCopyToHost":secureCopyToHost}) diff --git a/server/src/main/java/com/cloud/server/StatsCollector.java b/server/src/main/java/com/cloud/server/StatsCollector.java index ac1ae952cac..b2f244229e3 100644 --- a/server/src/main/java/com/cloud/server/StatsCollector.java +++ b/server/src/main/java/com/cloud/server/StatsCollector.java @@ -1377,6 +1377,20 @@ public class StatsCollector extends ManagerBase implements ComponentMethodInterc return imageStoreStats != null ? Math.max(0, imageStoreStats.getCapacityBytes() - imageStoreStats.getByteUsed()) : 0; } + /** + * 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 00000000000..282eee202cf --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/DiagnosticsHelper.java @@ -0,0 +1,80 @@ +// +// 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 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 21bb0a1889e..49ad2159698 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,67 @@ // under the License. package org.apache.cloudstack.diagnostics; +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.getTimeDifference; +import static org.apache.cloudstack.diagnostics.DiagnosticsHelper.umountSecondaryStorage; +import static org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesList.RouterDefaultSupportedFiles; +import static org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesList.SystemVMDefaultSupportedFiles; + +import java.io.File; 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 { +public class DiagnosticsServiceImpl extends ManagerBase implements PluggableService, DiagnosticsService, Configurable { private static final Logger LOGGER = Logger.getLogger(DiagnosticsServiceImpl.class); @Inject @@ -54,6 +88,39 @@ 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; + + // These 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 garbage collector background task to delete old files from secondary storage.", false); + private static final ConfigKey GarbageCollectionInterval = new ConfigKey<>("Advanced", Integer.class, + "diagnostics.data.gc.interval", "86400", + "The interval at which the garbage collector background tasks in seconds", false); + + // 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", "1800", + "Overall system VM script execution time out in seconds.", true); + private static final ConfigKey MaximumFileAgeforGarbageCollection = new ConfigKey<>("Advanced", Long.class, + "diagnostics.data.max.file.age", "86400", + "Sets the maximum time in seconds a file can stay in secondary storage before it is deleted.", true); + private static final ConfigKey DiskQuotaPercentageThreshold = new ConfigKey<>("Advanced", Double.class, + "diagnostics.data.disable.threshold", "0.9", + "Sets the secondary storage disk utilisation percentage for file retrieval. " + + "Used to look for suitable secondary storage with enough space, otherwise an exception is " + + "thrown when no secondary store is found.", true); @Override @ActionEvent(eventType = EventTypes.EVENT_SYSTEM_VM_DIAGNOSTICS, eventDescription = "running diagnostics on system vm", async = true) @@ -92,13 +159,13 @@ public class DiagnosticsServiceImpl extends ManagerBase implements PluggableServ 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 for system vm: " + vmInstance + ", on remote host: " + vmInstance.getHostName()); } } @@ -110,7 +177,6 @@ public class DiagnosticsServiceImpl extends ManagerBase implements PluggableServ final Pattern pattern = Pattern.compile(regex); return pattern.matcher(optionalArgs).find(); } - } protected String prepareShellCmd(String cmdType, String ipAddress, String optionalParams) { @@ -126,10 +192,334 @@ public class DiagnosticsServiceImpl extends ManagerBase implements PluggableServ } } + private String zipFilesInSystemVm(VMInstanceVO vmInstance, List optionalFilesList) { + List fileList = getFileListToBeRetrieved(optionalFilesList, vmInstance); + + if (CollectionUtils.isEmpty(fileList)) { + throw new CloudRuntimeException("Failed to generate diagnostics file list for retrieval."); + } + + final Answer zipFilesAnswer = prepareDiagnosticsFilesInSystemVm(vmInstance, fileList); + + if (zipFilesAnswer == null) { + throw new CloudRuntimeException(String.format("Failed to generate diagnostics zip file in the system VM %s", vmInstance.getUuid())); + } + + if (!zipFilesAnswer.getResult()) { + throw new CloudRuntimeException(String.format("Failed to generate diagnostics zip file in VM %s due to: %s", vmInstance.getUuid(), zipFilesAnswer.getDetails())); + } + + return zipFilesAnswer.getDetails().replace("\n", ""); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_SYSTEM_VM_DIAGNOSTICS, eventDescription = "getting diagnostics files on system vm", async = true) + public String getDiagnosticsDataCommand(GetDiagnosticsDataCmd cmd) { + final Long vmId = cmd.getId(); + final List optionalFilesList = cmd.getFilesList(); + final VMInstanceVO vmInstance = getSystemVMInstance(vmId); + final DataStore store = getImageStore(vmInstance.getDataCenterId()); + + final String zipFileInSystemVm = zipFilesInSystemVm(vmInstance, optionalFilesList); + final Long vmHostId = vmInstance.getHostId(); + copyZipFileToSecondaryStorage(vmInstance, vmHostId, zipFileInSystemVm, store); + deleteDiagnosticsZipFileInsystemVm(vmInstance, zipFileInSystemVm); + + // Now we need to create the file download URL + // Find ssvm of store + final long zoneId = vmInstance.getDataCenterId(); + 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_DIRECTORY + File.separator + 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 = copyToSecondaryStorageVMware(store, vmControlIp, fileToCopy); + } else { + copyResult = copyToSecondaryStorageNonVMware(store, vmControlIp, fileToCopy, vmHostId); + } + + if (!copyResult.first()) { + throw new CloudRuntimeException(String.format("Failed to copy %s to secondary storage %s due to: %s.", fileToCopy, store.getUri(), copyResult.second())); + } + + 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 prepareDiagnosticsFilesInSystemVm(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); + final Answer fileCleanupAnswer = agentManager.easySend(vmInstance.getHostId(), cmd); + 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())); + } + } + + return fileCleanupAnswer; + } + + /** + * 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 copyToSecondaryStorageNonVMware(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 copyToSecondaryStorageVMware(final DataStore store, final String vmSshIp, String diagnosticsFile) { + LOGGER.info(String.format("Copying %s from %s to secondary store %s", diagnosticsFile, vmSshIp, store.getUri())); + boolean success = false; + String mountPoint = mountManager.getMountPoint(store.getUri(), imageStoreDetailsUtil.getNfsVersion(store.getId())); + if (StringUtils.isBlank(mountPoint)) { + LOGGER.error("Failed to generate mount point for copying to secondary storage for " + store.getName()); + return new Pair<>(false, "Failed to mount secondary storage:" + store.getName()); + } + + // dirIn/mnt/SecStorage/uuid/diagnostics_data + String dataDirectoryInSecondaryStore = String.format("%s/%s", mountPoint, DIAGNOSTICS_DIRECTORY); + try { + File dataDirectory = new File(dataDirectoryInSecondaryStore); + boolean existsInSecondaryStore = dataDirectory.exists() || dataDirectory.mkdir(); + if (existsInSecondaryStore) { + // scp from system VM to mounted sec storage directory + File permKey = new File("/var/cloudstack/management/.ssh/id_rsa"); + SshHelper.scpFrom(vmSshIp, 3922, "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, e); + return new Pair<>(false, msg); + } finally { + umountSecondaryStorage(mountPoint); + } + + 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 DiskQuotaPercentageThreshold.value() 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.ZIP, 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; + } + + 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) { + final 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() && 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) { + cleanupOldDiagnosticFiles(store); + } + } + } + + @Override + public Long getDelay() { + // In Milliseconds + return GarbageCollectionInterval.value() * 1000L; + } + + private void cleanupOldDiagnosticFiles(DataStore store) { + String mountPoint = null; + try { + mountPoint = serviceImpl.mountManager.getMountPoint(store.getUri(), null); + if (StringUtils.isNotBlank(mountPoint)) { + File directory = new File(mountPoint + File.separator + DIAGNOSTICS_DIRECTORY); + if (directory.isDirectory()) { + deleteOldDiagnosticsFiles(directory, store.getName()); + } + } + } finally { + if (StringUtils.isNotBlank(mountPoint)) { + umountSecondaryStorage(mountPoint); + } + } + } + } + @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, + SystemVMDefaultSupportedFiles, + RouterDefaultSupportedFiles + }; + } } \ No newline at end of file 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 00000000000..cd9baa9f5d2 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesList.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.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 SystemVMDefaultSupportedFiles = new ConfigKey<>("Advanced", String.class, + "diagnostics.data.systemvm.defaults", "iptables,ipaddr,iprule,iproute,/etc/cloudstack-release," + + "/usr/local/cloud/systemvm/conf/agent.properties,/usr/local/cloud/systemvm/conf/consoleproxy.properties," + + "/var/log/cloud.log,/var/log/patchsystemvm.log,/var/log/daemon.log", + "List of supported diagnostics data file options for the CPVM and SSVM.", true); + + ConfigKey RouterDefaultSupportedFiles = new ConfigKey<>("Advanced", String.class, + "diagnostics.data.router.defaults", "iptables,ipaddr,iprule,iproute,/etc/cloudstack-release," + + "/etc/dnsmasq.conf,/etc/dhcphosts.txt,/etc/dhcpopts.txt,/etc/dnsmasq.d/cloud.conf,/etc/dnsmasq-resolv.conf,/var/lib/misc/dnsmasq.leases,/var/log/dnsmasq.log," + + "/etc/hosts,/etc/resolv.conf,/etc/haproxy/haproxy.cfg,/var/log/haproxy.log,/etc/ipsec.d/l2tp.conf,/var/log/cloud.log," + + "/var/log/routerServiceMonitor.log,/var/log/daemon.log", + "List of supported diagnostics data file options for the domain router.", 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 00000000000..b49da1d76b4 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DiagnosticsFilesListFactory.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.fileprocessor; + +import java.util.Collections; +import java.util.List; + +import com.cloud.vm.VirtualMachine; + +public class DiagnosticsFilesListFactory { + + public static DiagnosticsFilesList getDiagnosticsFilesList(List dataTypeList, VirtualMachine vm) { + final VirtualMachine.Type vmType = vm.getType(); + if (vmType == VirtualMachine.Type.ConsoleProxy || vmType == VirtualMachine.Type.SecondaryStorageVm) { + return new SystemVMDiagnosticsFiles(dataTypeList); + } else if (vmType == VirtualMachine.Type.DomainRouter) { + return new DomainRouterDiagnosticsFiles(dataTypeList); + } else { + return (DiagnosticsFilesList) Collections.emptyList(); + } + } +} 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 00000000000..b50c4faab28 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/DomainRouterDiagnosticsFiles.java @@ -0,0 +1,52 @@ +// +// 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 java.util.stream.Collectors; + +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.stream(RouterDefaultSupportedFiles.value().split(",")) + .map(String :: trim) + .distinct() + .collect(Collectors.toList())); + + } else { + filesList.addAll(dataTypeList); + } + return filesList; + } + +} diff --git a/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SystemVMDiagnosticsFiles.java b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SystemVMDiagnosticsFiles.java new file mode 100644 index 00000000000..4e123bf7d38 --- /dev/null +++ b/server/src/main/java/org/apache/cloudstack/diagnostics/fileprocessor/SystemVMDiagnosticsFiles.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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.collections.CollectionUtils; + +public class SystemVMDiagnosticsFiles implements DiagnosticsFilesList { + // Optional parameters + private List dataTypeList; + + public SystemVMDiagnosticsFiles(List dataTypeList) { + this.dataTypeList = dataTypeList; + } + + @Override + public List generateFileList() { + List filesList = new ArrayList<>(); + + if (CollectionUtils.isEmpty(dataTypeList)) { + filesList.addAll(Arrays.stream(SystemVMDefaultSupportedFiles.value().split(",")) + .map(String :: trim) + .distinct() + .collect(Collectors.toList())); + } 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 00000000000..7736e63a657 --- /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 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; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.to.DataObjectType; +import com.cloud.agent.api.to.DataTO; + +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 00000000000..115ee718fbe --- /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.ARCHIVE; + } + + @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 00000000000..a4e413ced9f --- /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 00000000000..50ef1365451 --- /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 2f67c4248d3..f3525cce6b1 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 00000000000..e0412db720f --- /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 static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.apache.cloudstack.diagnostics.fileprocessor.DiagnosticsFilesListFactory; +import org.apache.cloudstack.diagnostics.fileprocessor.DomainRouterDiagnosticsFiles; +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 com.cloud.vm.VMInstanceVO; +import com.cloud.vm.VirtualMachine; + +@RunWith(MockitoJUnitRunner.class) +public class DiagnosticsFilesListFactoryTest { + + private DomainRouterDiagnosticsFiles proxyDiagnosticFiles; + + @Mock + private VMInstanceVO vmInstance; + + @InjectMocks + private DiagnosticsFilesListFactory listFactory = new DiagnosticsFilesListFactory(); + + @Before + public void setUp() throws Exception { + Mockito.when(vmInstance.getType()).thenReturn(VirtualMachine.Type.DomainRouter); + } + + @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("ipaddr"); + + List files = Objects.requireNonNull(DiagnosticsFilesListFactory.getDiagnosticsFilesList(dataTypeList, vmInstance)).generateFileList(); + + assertEquals(files, dataTypeList); + } + + @Test + public void testDiagnosticsFileListDefaultsRouter() { + List filesList = Objects.requireNonNull(DiagnosticsFilesListFactory.getDiagnosticsFilesList(null, vmInstance)).generateFileList(); + + ConfigKey configKey = proxyDiagnosticFiles.RouterDefaultSupportedFiles; + 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 d85c5434d9a..04a7e8a2b0d 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.Mock; 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 class DiagnosticsServiceImplTest extends TestCase { 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 class DiagnosticsServiceImplTest extends TestCase { @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 class DiagnosticsServiceImplTest extends TestCase { "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 class DiagnosticsServiceImplTest extends TestCase { @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 00000000000..d14877badbc --- /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 00000000000..b95dfb5420c --- /dev/null +++ b/systemvm/debian/opt/cloud/bin/get_diagnostics_files.py @@ -0,0 +1,143 @@ +#!/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 logging +import os +import re +import shlex +import subprocess as sp +import sys +import time +import zipfile + + +# 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 + '.zip' + 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: + f = f.strip() + + if f in ('iptables', 'ipaddr', 'iprule', 'iproute'): + f = execute_shell_script(f) + files_from_shell_commands.append(f) + + if len(f) > 3 and f.startswith('[') and f.endswith(']'): + f = execute_shell_script(f[1:-1]) + 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 get_cmd(script): + if script is None or len(script) == 0: + return None + + cmd = None + if script == 'iptables': + cmd = 'iptables-save' + elif script == 'ipaddr': + cmd = 'ip address' + elif script == 'iprule': + cmd = 'ip rule list' + elif script == 'iproute': + cmd = 'ip route show table all' + else: + cmd = '/opt/cloud/bin/' + script + if not os.path.isfile(cmd.split(' ')[0]): + cmd = None + + return cmd + + +def execute_shell_script(script): + script = script.strip() + outputfile = script + '.log' + + with open(outputfile, 'wb', 0) as f: + try: + cmd = get_cmd(script) + if cmd is None: + f.write('Unable to generate command for ' + script + ', perhaps missing file') + else: + p = sp.Popen(cmd, shell=True, 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__': + fileList = sys.argv[1:] + zip_files(fileList) diff --git a/test/integration/smoke/test_diagnostics.py b/test/integration/smoke/test_diagnostics.py index 6364d83eeee..810dbb83093 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 @@ from marvin.lib.common import (get_domain, get_test_template, list_ssvms, list_routers) - +from marvin.lib.utils import (cleanup_resources) from nose.plugins.attrib import attr @@ -537,3 +538,197 @@ class TestRemoteDiagnostics(cloudstackTestCase): 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 18a70a2b89e..6831890ad86 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -12421,12 +12421,14 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it background-position: -69px -677px; } +.retrieveDiagnostics .icon, .downloadVolume .icon, .downloadTemplate .icon, .downloadISO .icon { background-position: -35px -125px; } +.retrieveDiagnostics:hover .icon, .downloadVolume:hover .icon, .downloadTemplate:hover .icon, .downloadISO:hover .icon { diff --git a/ui/css/src/scss/components/action-icons.scss b/ui/css/src/scss/components/action-icons.scss index 686b6e395bd..6ed07a30be3 100644 --- a/ui/css/src/scss/components/action-icons.scss +++ b/ui/css/src/scss/components/action-icons.scss @@ -347,6 +347,14 @@ 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 4ce59e08599..87deba8142b 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -243,6 +243,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....", @@ -802,6 +803,8 @@ var dictionary = { "label.gateway":"Gateway", "label.general.alerts":"General Alerts", "label.generating.url":"Generating URL", +"label.get.diagnostics.desc":"If you wish to override the standard files returned, enter them here. Otherwise leave blank and press OK", +"label.get.diagnostics.files":"Alternate Files to Retrieve", "label.globo.dns":"GloboDNS", "label.globo.dns.configuration":"GloboDNS Configuration", "label.gluster.volume":"Volume", @@ -2149,9 +2152,10 @@ var dictionary = { "message.disabling.network.offering":"Disabling network offering", "message.disabling.vpc.offering":"Disabling VPC offering", "message.disallowed.characters":"Disallowed characters: <,>", -"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", +"message.download.diagnostics":"Please click the link to download the retrieved diagnostics:

00000", +"message.download.ISO":"Please click the link to download the ISO:

00000", +"message.download.template":"Please click the link to download the template:

00000", +"message.download.volume":"Please click the link to download the volume:

00000", "message.download.volume.confirm":"Please confirm that you want to download this volume.", "message.edit.account":"Edit (\"-1\" indicates no limit to the amount of resources create)", "message.edit.confirm":"Please confirm your changes before clicking \"Save\".", diff --git a/ui/scripts/system.js b/ui/scripts/system.js index 1a73e64f3df..2ae2f466043 100755 --- a/ui/scripts/system.js +++ b/ui/scripts/system.js @@ -3933,6 +3933,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: 'label.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: { @@ -8847,6 +8897,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: { @@ -10293,6 +10393,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: 'label.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: { @@ -11643,6 +11793,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: 'label.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: { @@ -22072,6 +22272,7 @@ if (isAdmin()) { allowedActions.push("migrate"); allowedActions.push("diagnostics"); + allowedActions.push("retrieveDiagnostics"); } } else if (jsonObj.state == 'Stopped') { allowedActions.push("start"); @@ -22123,6 +22324,7 @@ if (isAdmin()) { allowedActions.push("migrate"); allowedActions.push("diagnostics"); + allowedActions.push("retrieveDiagnostics"); } } else if (jsonObj.state == 'Stopped') { allowedActions.push("start"); 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 88be5774225..042842064df 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 class SshHelper { 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 {