Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fe3df6b

Browse filesBrowse files
committed
Merge branch '4.22' into 'main'
2 parents 38b674f + 17e5947 commit fe3df6b
Copy full SHA for fe3df6b

6 files changed

+1,609-87Lines changed: 1609 additions & 87 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

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

Copy file name to clipboardExpand all lines: plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMStorageProcessor.java
+5-4Lines 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);
Collapse file

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

Copy file name to clipboardExpand all lines: plugins/hypervisors/kvm/src/main/java/org/apache/cloudstack/utils/qemu/QemuImg.java
+4-1Lines 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

Collapse file

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

Copy file name to clipboardExpand all lines: plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/qemu/QemuImgTest.java
+15Lines 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";
Collapse file

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

Copy file name to clipboardExpand all lines: plugins/storage/volume/flasharray/src/main/java/org/apache/cloudstack/storage/datastore/adapter/flasharray/FlashArrayAdapter.java
+136-48Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.