Skip to content

Commit fe3df6b

Browse files
committed
Merge branch '4.22' into 'main'
2 parents 38b674f + 17e5947 commit fe3df6b

6 files changed

Lines changed: 1609 additions & 87 deletions

File tree

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2319,23 +2319,24 @@ protected void rebaseSnapshot(SnapshotObjectTO snapshotObjectTO, KVMStoragePool
23192319

23202320
logger.debug("Rebasing snapshot [{}] with parent [{}].", snapshotName, parentSnapshotPath);
23212321

2322+
long snapshotTimeoutInMillis = wait * 1000L;
23222323
try {
2323-
QemuImg qemuImg = new QemuImg(wait);
2324+
QemuImg qemuImg = new QemuImg(snapshotTimeoutInMillis);
23242325
qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false);
23252326
} catch (LibvirtException | QemuImgException e) {
23262327
if (!StringUtils.contains(e.getMessage(), "Is another process using the image")) {
23272328
logger.error("Exception while rebasing incremental snapshot [{}] due to: [{}].", snapshotName, e.getMessage(), e);
23282329
throw new CloudRuntimeException(e);
23292330
}
2330-
retryRebase(snapshotName, wait, e, snapshotFile, parentSnapshotFile);
2331+
retryRebase(snapshotName, snapshotTimeoutInMillis, e, snapshotFile, parentSnapshotFile);
23312332
}
23322333
}
23332334

2334-
private void retryRebase(String snapshotName, int wait, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) {
2335+
private void retryRebase(String snapshotName, long waitInMilliseconds, Exception e, QemuImgFile snapshotFile, QemuImgFile parentSnapshotFile) {
23352336
logger.warn("Libvirt still has not released the lock, will wait [{}] milliseconds and try again later.", incrementalSnapshotRetryRebaseWait);
23362337
try {
23372338
Thread.sleep(incrementalSnapshotRetryRebaseWait);
2338-
QemuImg qemuImg = new QemuImg(wait);
2339+
QemuImg qemuImg = new QemuImg(waitInMilliseconds);
23392340
qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false);
23402341
} catch (LibvirtException | QemuImgException | InterruptedException ex) {
23412342
logger.error("Unable to rebase snapshot [{}].", snapshotName, ex);

plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,10 @@ public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) {
927927
}
928928

929929
protected static boolean helpSupportsImageFormat(String text, QemuImg.PhysicalDiskFormat format) {
930-
Pattern pattern = Pattern.compile("Supported\\sformats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE);
930+
// QEMU >= 10.1.0 changed the qemu-img --help header from
931+
// "Supported formats:" to "Supported image formats:", so the word
932+
// "image" must be treated as optional here.
933+
Pattern pattern = Pattern.compile("Supported\\s(image\\s)?formats:[a-zA-Z0-9-_\\s]*?\\b" + format + "\\b", CASE_INSENSITIVE);
931934
return pattern.matcher(text).find();
932935
}
933936

plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,21 @@ public void testHelpSupportsImageFormat() throws QemuImgException, LibvirtExcept
390390
Assert.assertFalse("should not support http", QemuImg.helpSupportsImageFormat(partialHelp, PhysicalDiskFormat.SHEEPDOG));
391391
}
392392

393+
@Test
394+
public void testHelpSupportsImageFormatQemu101Header() throws QemuImgException, LibvirtException {
395+
// qemu-img 10.1.0 (e.g. RHEL 9.8: qemu-kvm-10.1.0-17.el9_8.3) changed the
396+
// help header from "Supported formats:" to "Supported image formats:"
397+
String help = "Supported image formats:\n" +
398+
" blkdebug blklogwrites blkverify compress copy-before-write copy-on-read\n" +
399+
" file ftp ftps host_cdrom host_device http https io_uring luks nbd null-aio\n" +
400+
" null-co nvme nvme-io_uring preallocate qcow2 quorum raw rbd\n" +
401+
" snapshot-access throttle vdi vhdx virtio-blk-vfio-pci\n" +
402+
" virtio-blk-vhost-user virtio-blk-vhost-vdpa vmdk vpc\n";
403+
Assert.assertTrue("should support luks", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.LUKS));
404+
Assert.assertTrue("should support qcow2", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.QCOW2));
405+
Assert.assertFalse("should not support sheepdog", QemuImg.helpSupportsImageFormat(help, PhysicalDiskFormat.SHEEPDOG));
406+
}
407+
393408
@Test
394409
public void testCheckAndRepair() throws LibvirtException {
395410
String filename = "/tmp/" + UUID.randomUUID() + ".qcow2";

plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java

Lines changed: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.ArrayList;
2929
import java.util.HashMap;
3030
import java.util.Map;
31+
import java.util.Set;
32+
import java.util.concurrent.ConcurrentHashMap;
3133

3234
import javax.net.ssl.HostnameVerifier;
3335
import javax.net.ssl.SSLContext;
@@ -63,6 +65,7 @@
6365
import com.cloud.utils.exception.CloudRuntimeException;
6466
import com.fasterxml.jackson.core.JsonProcessingException;
6567
import com.fasterxml.jackson.core.type.TypeReference;
68+
import com.fasterxml.jackson.databind.JsonNode;
6669
import com.fasterxml.jackson.databind.ObjectMapper;
6770
import org.apache.logging.log4j.LogManager;
6871
import org.apache.logging.log4j.Logger;
@@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter {
8790
private static final String API_LOGIN_VERSION_DEFAULT = "1.19";
8891
private static final String API_VERSION_DEFAULT = "2.23";
8992

93+
// URLs for which the legacy-auth deprecation WARN has already been emitted,
94+
// so we don't spam the logs once per refresh per pool while it's still configured.
95+
private static final Set<String> WARNED_LEGACY_URLS = ConcurrentHashMap.newKeySet();
96+
9097
static final ObjectMapper mapper = new ObjectMapper();
9198
public String pod = null;
9299
public String hostgroup = null;
@@ -589,6 +596,91 @@ private String getAccessToken() {
589596
return accessToken;
590597
}
591598

599+
/**
600+
* Discover the latest supported Purity REST API version by hitting the unauthenticated
601+
* {@code /api/api_version} endpoint (returns {@code {"version":["1.0",...,"2.36"]}}).
602+
* The discovered version is stored on {@link #apiVersion}; on failure the caller-configured
603+
* default remains in place.
604+
*/
605+
private void fetchApiVersionFromPurity(CloseableHttpClient client) {
606+
HttpGet vReq = new HttpGet(url + "/api_version");
607+
CloseableHttpResponse vResp = null;
608+
try {
609+
vResp = client.execute(vReq);
610+
if (vResp.getStatusLine().getStatusCode() == 200) {
611+
JsonNode root = mapper.readTree(vResp.getEntity().getContent());
612+
JsonNode versions = root.get("version");
613+
if (versions != null && versions.isArray() && versions.size() > 0) {
614+
apiVersion = versions.get(versions.size() - 1).asText();
615+
}
616+
} else {
617+
logger.warn("Unexpected HTTP " + vResp.getStatusLine().getStatusCode()
618+
+ " from FlashArray [" + url + "] /api_version, falling back to default "
619+
+ API_VERSION_DEFAULT);
620+
}
621+
} catch (Exception e) {
622+
logger.warn("Failed to discover Purity REST API version from " + url
623+
+ "/api_version, falling back to default " + API_VERSION_DEFAULT, e);
624+
} finally {
625+
if (vResp != null) {
626+
try {
627+
vResp.close();
628+
} catch (IOException e) {
629+
logger.debug("Error closing /api_version response from FlashArray [" + url + "]", e);
630+
}
631+
}
632+
}
633+
}
634+
635+
/**
636+
* Exchange the operator-configured username/password for a long-lived Purity api-token
637+
* via REST 1.x {@code /auth/apitoken}. Emits the once-per-URL deprecation WARN.
638+
* @return the api-token to feed into the REST 2.x /login exchange.
639+
*/
640+
private String getApiTokenUsingUserPass(CloseableHttpClient client) throws IOException {
641+
if (WARNED_LEGACY_URLS.add(url)) {
642+
logger.warn("FlashArray adapter at [" + url + "] is using deprecated username/password "
643+
+ "login against Purity REST 1.x. Replace with a pre-minted "
644+
+ ProviderAdapter.API_TOKEN_KEY + " detail; the username/password code path will be "
645+
+ "removed in a future release.");
646+
}
647+
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
648+
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
649+
postParms.add(new BasicNameValuePair("username", username));
650+
postParms.add(new BasicNameValuePair("password", password));
651+
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
652+
CloseableHttpResponse response = null;
653+
try {
654+
response = client.execute(request);
655+
int statusCode = response.getStatusLine().getStatusCode();
656+
if (statusCode == 200 || statusCode == 201) {
657+
FlashArrayApiToken legacyToken = mapper.readValue(response.getEntity().getContent(),
658+
FlashArrayApiToken.class);
659+
if (legacyToken == null || legacyToken.getApiToken() == null) {
660+
throw new CloudRuntimeException(
661+
"Authentication responded successfully but no api token was returned");
662+
}
663+
return legacyToken.getApiToken();
664+
} else if (statusCode == 401 || statusCode == 403) {
665+
throw new CloudRuntimeException(
666+
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
667+
+ "] failed, unable to retrieve session token");
668+
} else {
669+
throw new CloudRuntimeException(
670+
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
671+
+ "] - " + response.getStatusLine().getReasonPhrase());
672+
}
673+
} finally {
674+
if (response != null) {
675+
try {
676+
response.close();
677+
} catch (IOException e) {
678+
logger.debug("Error closing legacy auth/apitoken response from FlashArray [" + url + "]", e);
679+
}
680+
}
681+
}
682+
}
683+
592684
private synchronized void refreshSession(boolean force) {
593685
try {
594686
if (force || keyExpiration < System.currentTimeMillis()) {
@@ -663,9 +755,11 @@ private void login() {
663755
}
664756

665757
apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION);
666-
if (apiVersion == null) {
758+
boolean apiVersionExplicit = apiVersion != null;
759+
if (!apiVersionExplicit) {
667760
apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION);
668-
if (apiVersion == null) {
761+
apiVersionExplicit = apiVersion != null;
762+
if (!apiVersionExplicit) {
669763
apiVersion = API_VERSION_DEFAULT;
670764
}
671765
}
@@ -732,72 +826,66 @@ private void login() {
732826
skipTlsValidation = true;
733827
}
734828

829+
// Resolve the long-lived API token. Prefer a pre-minted api_token (Purity REST 2.x flow);
830+
// fall back to legacy username/password auth via Purity REST 1.x for backward compatibility.
831+
String apiToken = connectionDetails.get(ProviderAdapter.API_TOKEN_KEY);
832+
if (apiToken != null && apiToken.isEmpty()) {
833+
apiToken = null;
834+
}
835+
boolean usingLegacyUserPass = apiToken == null;
836+
if (usingLegacyUserPass && (username == null || password == null)) {
837+
throw new CloudRuntimeException("FlashArray adapter requires either " + ProviderAdapter.API_TOKEN_KEY
838+
+ " (preferred) or both " + ProviderAdapter.API_USERNAME_KEY + " and "
839+
+ ProviderAdapter.API_PASSWORD_KEY + " in the connection details");
840+
}
841+
842+
CloseableHttpClient client = getClient();
735843
CloseableHttpResponse response = null;
736844
try {
737-
HttpPost request = new HttpPost(url + "/" + apiLoginVersion + "/auth/apitoken");
738-
// request.addHeader("Content-Type", "application/json");
739-
// request.addHeader("Accept", "application/json");
740-
ArrayList<NameValuePair> postParms = new ArrayList<NameValuePair>();
741-
postParms.add(new BasicNameValuePair("username", username));
742-
postParms.add(new BasicNameValuePair("password", password));
743-
request.setEntity(new UrlEncodedFormEntity(postParms, "UTF-8"));
744-
CloseableHttpClient client = getClient();
745-
response = (CloseableHttpResponse) client.execute(request);
746-
747-
int statusCode = response.getStatusLine().getStatusCode();
748-
FlashArrayApiToken apitoken = null;
749-
if (statusCode == 200 | statusCode == 201) {
750-
apitoken = mapper.readValue(response.getEntity().getContent(), FlashArrayApiToken.class);
751-
if (apitoken == null) {
752-
throw new CloudRuntimeException(
753-
"Authentication responded successfully but no api token was returned");
754-
}
755-
} else if (statusCode == 401 || statusCode == 403) {
756-
throw new CloudRuntimeException(
757-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
758-
+ "] failed, unable to retrieve session token");
759-
} else {
760-
throw new CloudRuntimeException(
761-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
762-
+ "] - " + response.getStatusLine().getReasonPhrase());
845+
// Discover the latest supported API version from the array unless one was explicitly configured.
846+
// GET /api/api_version is unauthenticated and returns {"version":["1.0",...,"2.36"]}.
847+
if (!apiVersionExplicit) {
848+
fetchApiVersionFromPurity(client);
763849
}
764850

765-
// now we need to get the access token
766-
request = new HttpPost(url + "/" + apiVersion + "/login");
767-
request.addHeader("api-token", apitoken.getApiToken());
768-
response = (CloseableHttpResponse) client.execute(request);
851+
if (usingLegacyUserPass) {
852+
apiToken = getApiTokenUsingUserPass(client);
853+
}
769854

770-
statusCode = response.getStatusLine().getStatusCode();
771-
if (statusCode == 200 | statusCode == 201) {
855+
// Exchange the long-lived api-token for a short-lived x-auth-token (REST 2.x).
856+
HttpPost request = new HttpPost(url + "/" + apiVersion + "/login");
857+
request.addHeader("api-token", apiToken);
858+
response = client.execute(request);
859+
int statusCode = response.getStatusLine().getStatusCode();
860+
if (statusCode == 200 || statusCode == 201) {
772861
Header[] headers = response.getHeaders("x-auth-token");
773862
if (headers == null || headers.length == 0) {
774863
throw new CloudRuntimeException(
775-
"Getting access token responded successfully but access token was not available");
864+
"FlashArray /login responded successfully but no x-auth-token header was returned");
776865
}
777866
accessToken = headers[0].getValue();
778867
} else if (statusCode == 401 || statusCode == 403) {
779868
throw new CloudRuntimeException(
780-
"Authentication or Authorization to FlashArray [" + url + "] with user [" + username
781-
+ "] failed, unable to retrieve session token");
869+
"FlashArray [" + url + "] rejected the api-token at /" + apiVersion + "/login");
782870
} else {
783871
throw new CloudRuntimeException(
784-
"Unexpected HTTP response code from FlashArray [" + url + "] - [" + statusCode
785-
+ "] - " + response.getStatusLine().getReasonPhrase());
872+
"Unexpected HTTP response code from FlashArray [" + url + "] /" + apiVersion
873+
+ "/login - [" + statusCode + "] - "
874+
+ response.getStatusLine().getReasonPhrase());
786875
}
787-
788876
} catch (UnsupportedEncodingException e) {
789-
throw new CloudRuntimeException("Error creating input for login, check username/password encoding");
877+
throw new CloudRuntimeException("Error encoding login form for FlashArray [" + url + "]", e);
790878
} catch (UnsupportedOperationException e) {
791879
throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e);
792880
} catch (IOException e) {
793881
throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e);
794882
} finally {
795-
try {
796-
if (response != null) {
883+
if (response != null) {
884+
try {
797885
response.close();
886+
} catch (IOException e) {
887+
logger.debug("Error closing response from login attempt to FlashArray", e);
798888
}
799-
} catch (IOException e) {
800-
logger.debug("Error closing response from login attempt to FlashArray", e);
801889
}
802890
}
803891
}
@@ -965,7 +1053,7 @@ private <T> T PATCH(String path, Object input, final TypeReference<T> type) {
9651053
request.setEntity(new StringEntity(data));
9661054

9671055
CloseableHttpClient client = getClient();
968-
response = (CloseableHttpResponse) client.execute(request);
1056+
response = client.execute(request);
9691057

9701058
final int statusCode = response.getStatusLine().getStatusCode();
9711059
if (statusCode == 200 || statusCode == 201) {
@@ -1020,7 +1108,7 @@ private <T> T GET(String path, final TypeReference<T> type) {
10201108
request.addHeader("X-auth-token", getAccessToken());
10211109

10221110
CloseableHttpClient client = getClient();
1023-
response = (CloseableHttpResponse) client.execute(request);
1111+
response = client.execute(request);
10241112
final int statusCode = response.getStatusLine().getStatusCode();
10251113
if (statusCode == 200) {
10261114
try {
@@ -1062,7 +1150,7 @@ private void DELETE(String path) {
10621150
request.addHeader("X-auth-token", getAccessToken());
10631151

10641152
CloseableHttpClient client = getClient();
1065-
response = (CloseableHttpResponse) client.execute(request);
1153+
response = client.execute(request);
10661154
final int statusCode = response.getStatusLine().getStatusCode();
10671155
if (statusCode == 200 || statusCode == 404 || statusCode == 400) {
10681156
// this means the volume was deleted successfully, or doesn't exist (effective

0 commit comments

Comments
 (0)