Skip to content

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.feign.client;

import feign.Headers;
import feign.Param;
import feign.RequestLine;
import org.apache.cloudstack.storage.feign.model.EmsApplicationLog;

public interface EmsFeignClient {

@RequestLine("POST /api/support/ems/application-logs")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
void sendEmsApplicationLog(@Param("authHeader") String authHeader, EmsApplicationLog emsApplicationLog);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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.feign.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class EmsApplicationLog {

@JsonProperty("computer_name")
private String computerName;

@JsonProperty("event_source")
private String eventSource;

@JsonProperty("app_version")
private String appVersion;

@JsonProperty("category")
private String category;

@JsonProperty("severity")
private String severity;

@JsonProperty("autosupport_required")
private Boolean autosupportRequired;

@JsonProperty("event_id")
private String eventId;

@JsonProperty("event_description")
private String eventDescription;

public EmsApplicationLog() {
}

public String getComputerName() {
return computerName;
}

public void setComputerName(String computerName) {
this.computerName = computerName;
}

public String getEventSource() {
return eventSource;
}

public void setEventSource(String eventSource) {
this.eventSource = eventSource;
}

public String getAppVersion() {
return appVersion;
}

public void setAppVersion(String appVersion) {
this.appVersion = appVersion;
}

public String getCategory() {
return category;
}

public void setCategory(String category) {
this.category = category;
}

public String getSeverity() {
return severity;
}

public void setSeverity(String severity) {
this.severity = severity;
}

public Boolean getAutosupportRequired() {
return autosupportRequired;
}

public void setAutosupportRequired(Boolean autosupportRequired) {
this.autosupportRequired = autosupportRequired;
}

public String getEventId() {
return eventId;
}

public void setEventId(String eventId) {
this.eventId = eventId;
}

public String getEventDescription() {
return eventDescription;
}

public void setEventDescription(String eventDescription) {
this.eventDescription = eventDescription;
}

@Override
public String toString() {
return "EmsApplicationLog{" +
"computerName='" + computerName + '\'' +
", eventSource='" + eventSource + '\'' +
", appVersion='" + appVersion + '\'' +
", category='" + category + '\'' +
", severity='" + severity + '\'' +
", autosupportRequired=" + autosupportRequired +
", eventId='" + eventId + '\'' +
", eventDescription='" + eventDescription + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,25 @@

import org.apache.cloudstack.storage.feign.FeignClientFactory;
import org.apache.cloudstack.storage.feign.client.AggregateFeignClient;
import org.apache.cloudstack.storage.feign.client.ClusterFeignClient;
import org.apache.cloudstack.storage.feign.client.JobFeignClient;
import org.apache.cloudstack.storage.feign.client.NASFeignClient;
import org.apache.cloudstack.storage.feign.client.NetworkFeignClient;
import org.apache.cloudstack.storage.feign.client.SANFeignClient;
import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
import org.apache.cloudstack.storage.feign.client.EmsFeignClient;
import org.apache.cloudstack.storage.feign.client.SvmFeignClient;
import org.apache.cloudstack.storage.feign.client.VolumeFeignClient;
import org.apache.cloudstack.storage.feign.model.Aggregate;
import org.apache.cloudstack.storage.feign.model.Cluster;
import org.apache.cloudstack.storage.feign.model.EmsApplicationLog;
import org.apache.cloudstack.storage.feign.model.IpInterface;
import org.apache.cloudstack.storage.feign.model.IscsiService;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.Nas;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.feign.model.Version;
import org.apache.cloudstack.storage.feign.model.Volume;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
Expand Down Expand Up @@ -74,6 +79,8 @@ public abstract class StorageStrategy {
protected SANFeignClient sanFeignClient;
protected NASFeignClient nasFeignClient;
protected SnapshotFeignClient snapshotFeignClient;
protected ClusterFeignClient clusterFeignClient;
protected EmsFeignClient emsFeignClient;

protected OntapStorage storage;

Expand Down Expand Up @@ -105,6 +112,74 @@ public StorageStrategy(OntapStorage ontapStorage) {
this.sanFeignClient = feignClientFactory.createClient(SANFeignClient.class, baseURL);
this.nasFeignClient = feignClientFactory.createClient(NASFeignClient.class, baseURL);
this.snapshotFeignClient = feignClientFactory.createClient(SnapshotFeignClient.class, baseURL);
this.clusterFeignClient = feignClientFactory.createClient(ClusterFeignClient.class, baseURL);
this.emsFeignClient = feignClientFactory.createClient(EmsFeignClient.class, baseURL);
}

/**
* Fetches the full ONTAP {@link Cluster} object (name, uuid, version) in a single REST call,
* for ASUP telemetry. Best-effort: returns {@code null} if it cannot be retrieved, and callers
* must never fail a storage operation because of it.
*
* @return the ONTAP {@link Cluster}, or {@code null} if it cannot be resolved
*/
public Cluster getClusterInfo() {
try {
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
return clusterFeignClient.getCluster(authHeader, true);
} catch (Exception e) {
logger.warn("getClusterInfo: failed to fetch ONTAP cluster info for storage IP {}: {}",
storage.getStorageIP(), e.getMessage());
return null;
}
}

/**
* Extracts a clean, parser-friendly ONTAP version string from a {@link Cluster}.
*
* <p>Prefers the compact "generation.major.minor" numeric form (e.g. "9.17.1"), which avoids
* the colon/date noise in {@code version.full}. Falls back to the verbose {@code version.full}
* banner only when the numeric fields are unavailable.</p>
*
* @param cluster the cluster (may be {@code null})
* @return the ONTAP version string, or {@code null} if it cannot be resolved
*/
public String getClusterVersion(Cluster cluster) {
if (cluster == null || cluster.getVersion() == null) {
return null;
}
Version version = cluster.getVersion();
if (version.getGeneration() != null && version.getMajor() != null && version.getMinor() != null) {
return version.getGeneration() + OntapStorageConstants.DOT + version.getMajor()
+ OntapStorageConstants.DOT + version.getMinor();
}
if (version.getFull() != null && !version.getFull().isEmpty()) {
return version.getFull();
}
return null;
}

/**
* Pushes a single ASUP (AutoSupport) EMS application-log message to the ONTAP cluster.
*
* <p>This is strictly best-effort telemetry: any failure is logged and swallowed so that
* it can never affect a storage operation or the periodic scheduler.</p>
*
* @param message the EMS message to send
*/
public void sendAsupMessage(EmsApplicationLog message) {
if (message == null) {
return;
}
try {
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
emsFeignClient.sendEmsApplicationLog(authHeader, message);
logger.debug("sendAsupMessage: ASUP EMS message [event-id={}] sent to ONTAP cluster at {}",
message.getEventId(), storage.getStorageIP());
} catch (Exception e) {
logger.error("sendAsupMessage: failed to send ASUP EMS message [event-id={}] to ONTAP cluster at {}: {}",
message.getEventId(), storage.getStorageIP(), e.getMessage());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class OntapStorageConstants {
public static final String SEMICOLON = ";";
public static final String COMMA = ",";
public static final String HYPHEN = "-";
public static final String DOT = ".";

public static final String VOLUME_PATH_PREFIX = "/vol/";

Expand Down Expand Up @@ -130,4 +131,25 @@ public class OntapStorageConstants {

/** vm_snapshot_details key for ONTAP FlexVolume-level VM snapshots. */
public static final String ONTAP_FLEXVOL_SNAPSHOT = "ontapFlexVolSnapshot";

// ASUP (AutoSupport) / EMS telemetry
public static final String ADVANCED_CONFIG_KEY_CATEGORY = "Advanced";
public static final String ASUP_CATEGORY = "provisioning";
public static final String ASUP_SEVERITY = "notice";
public static final String ASUP_EVENT_SOURCE = "CloudStack ONTAP plugin";
public static final String ASUP_EVENT_ID_HEARTBEAT = "0";
public static final String ASUP_EVENT_ID_STORAGE_POOL = "1";
public static final String ASUP_UNKNOWN = "unknown";
/** Event-id 0 field: VM snapshots spanning multiple ONTAP pools (consistency group). */
public static final String ASUP_SNAPSHOT_ACROSS_POOL = "snapshot_across_pool";
public static final String ASUP_GLOBAL_LOCK_NAME = "ontap.asup.push";
public static final String ASUP_ENABLED_CONFIG_KEY = "ontap.asup.enabled";
public static final String ASUP_ENABLED_DEFAULT = "true";
public static final String ASUP_ENABLED_DESCRIPTION = "Enable periodic ASUP (AutoSupport) telemetry push from the CloudStack ONTAP plugin to the ONTAP cluster. Set to true to enable or false to disable.";
public static final String ASUP_INTERVAL_CONFIG_KEY = "ontap.asup.interval";
public static final int ASUP_MIN_INTERVAL_SECONDS = 3600; // 1 hour
public static final int ASUP_MAX_INTERVAL_SECONDS = 86400; // 24 hours
public static final int ASUP_DEFAULT_INTERVAL_SECONDS = 43200; // 12 hours (twice a day)
public static final String ASUP_INTERVAL_DESCRIPTION = "Interval (in seconds) between periodic ASUP telemetry pushes from the CloudStack ONTAP plugin. "
+ "Allowed range: 3600-86400 (1 hour to 24 hours). Default: 43200 (12 hours).";
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@
<bean id="ontapVMSnapshotStrategy"
class="org.apache.cloudstack.storage.vmsnapshot.OntapVMSnapshotStrategy" />

<bean id="ontapAsupManager"
class="org.apache.cloudstack.storage.asup.OntapAsupManager" />

</beans>
Loading
Loading