Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ public interface VolumeDao extends GenericDao<VolumeVO, Long>, StateDao<Volume.S

boolean existsWithKmsKey(long kmsKeyId);

/**
* Returns true if any VM with a non-destroyed ROOT volume on {@code poolId} also has a
* non-destroyed DATADISK on a different primary storage pool. Existence check only
* ({@code LIMIT 1}); does not load volumes into memory.
*/
boolean hasMultiPrimaryStoragePoolVm(long poolId);

/**
* Retrieves volume by its externalId
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
protected static final String SELECT_HYPERTYPE_FROM_CLUSTER_VOLUME = "SELECT c.hypervisor_type from volumes v, storage_pool s, cluster c where v.pool_id = s.id and s.cluster_id = c.id and v.id = ?";
protected static final String SELECT_HYPERTYPE_FROM_ZONE_VOLUME = "SELECT s.hypervisor from volumes v, storage_pool s where v.pool_id = s.id and v.id = ?";
protected static final String SELECT_POOLSCOPE = "SELECT s.scope from storage_pool s, volumes v where s.id = v.pool_id and v.id = ?";
private static final String HAS_MULTI_PRIMARY_STORAGE_POOL_VM =
"SELECT 1 FROM volumes root INNER JOIN volumes data ON data.instance_id = root.instance_id "
+ "WHERE root.pool_id = ? AND root.volume_type = 'ROOT' AND root.instance_id IS NOT NULL "
+ "AND root.removed IS NULL AND root.state NOT IN ('Destroy', 'Expunged') "
+ "AND data.volume_type = 'DATADISK' AND data.pool_id IS NOT NULL AND data.pool_id <> ? "
+ "AND data.removed IS NULL AND data.state NOT IN ('Destroy', 'Expunged') LIMIT 1";

private static final String ORDER_POOLS_NUMBER_OF_VOLUMES_FOR_ACCOUNT_PART1 = "SELECT pool.id, SUM(IF(vol.state='Ready' AND vol.account_id = ?, 1, 0)) FROM `cloud`.`storage_pool` pool LEFT JOIN `cloud`.`volumes` vol ON pool.id = vol.pool_id WHERE pool.data_center_id = ? ";
private static final String ORDER_POOLS_NUMBER_OF_VOLUMES_FOR_ACCOUNT_PART2 = " GROUP BY pool.id ORDER BY 2 ASC ";
Expand Down Expand Up @@ -998,6 +1004,21 @@ public boolean existsWithKmsKey(long kmsKeyId) {
return findOneBy(sc) != null;
}

@Override
@DB
public boolean hasMultiPrimaryStoragePoolVm(long poolId) {
TransactionLegacy txn = TransactionLegacy.currentTxn();
try (PreparedStatement pstmt = txn.prepareAutoCloseStatement(HAS_MULTI_PRIMARY_STORAGE_POOL_VM)) {
pstmt.setLong(1, poolId);
pstmt.setLong(2, poolId);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
throw new CloudRuntimeException("DB Exception on: " + HAS_MULTI_PRIMARY_STORAGE_POOL_VM, e);
}
}

public VolumeVO findByExternalUuid(String externalUuid) {
SearchCriteria<VolumeVO> sc = ExternalUuidSearch.create();
sc.setParameters("externalUuid", externalUuid);
Expand Down
5 changes: 5 additions & 0 deletions plugins/storage/volume/ontap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
<artifactId>cloud-engine-storage-volume</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-cluster</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
Expand Down

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
Loading
Loading