| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2319,23 +2319,24 @@ protected void rebaseSnapshot(SnapshotObjectTO snapshotObjectTO, KVMStoragePool | |||
| 2319 | 2319 | ||
| 2320 | 2320 | logger.debug("Rebasing snapshot [{}] with parent [{}].", snapshotName, parentSnapshotPath); | |
| 2321 | 2321 | ||
| 2322 | + long snapshotTimeoutInMillis = wait * 1000L; | ||
| 2322 | 2323 | try { | |
| 2323 | - QemuImg qemuImg = new QemuImg(wait); | ||
| 2324 | + QemuImg qemuImg = new QemuImg(snapshotTimeoutInMillis); | ||
| 2324 | 2325 | qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); | |
| 2325 | 2326 | } catch (LibvirtException | QemuImgException e) { | |
| 2326 | 2327 | if (!StringUtils.contains(e.getMessage(), "Is another process using the image")) { | |
| 2327 | 2328 | logger.error("Exception while rebasing incremental snapshot [{}] due to: [{}].", snapshotName, e.getMessage(), e); | |
| 2328 | 2329 | throw new CloudRuntimeException(e); | |
| 2329 | 2330 | } | |
| 2330 | - retryRebase(snapshotName, wait, e, snapshotFile, parentSnapshotFile); | ||
| 2331 | + retryRebase(snapshotName, snapshotTimeoutInMillis, e, snapshotFile, parentSnapshotFile); | ||
| 2331 | 2332 | } | |
| 2332 | 2333 | } | |
| 2333 | 2334 | ||
| 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) { | ||
| 2335 | 2336 | logger.warn("Libvirt still has not released the lock, will wait [{}] milliseconds and try again later.", incrementalSnapshotRetryRebaseWait); | |
| 2336 | 2337 | try { | |
| 2337 | 2338 | Thread.sleep(incrementalSnapshotRetryRebaseWait); | |
| 2338 | - QemuImg qemuImg = new QemuImg(wait); | ||
| 2339 | + QemuImg qemuImg = new QemuImg(waitInMilliseconds); | ||
| 2339 | 2340 | qemuImg.rebase(snapshotFile, parentSnapshotFile, PhysicalDiskFormat.QCOW2.toString(), false); | |
| 2340 | 2341 | } catch (LibvirtException | QemuImgException | InterruptedException ex) { | |
| 2341 | 2342 | logger.error("Unable to rebase snapshot [{}].", snapshotName, ex); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -927,7 +927,10 @@ public boolean supportsImageFormat(QemuImg.PhysicalDiskFormat format) { | |||
| 927 | 927 | } | |
| 928 | 928 | ||
| 929 | 929 | 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); | ||
| 931 | 934 | return pattern.matcher(text).find(); | |
| 932 | 935 | } | |
| 933 | 936 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -390,6 +390,21 @@ public void testHelpSupportsImageFormat() throws QemuImgException, LibvirtExcept | |||
| 390 | 390 | Assert.assertFalse("should not support http", QemuImg.helpSupportsImageFormat(partialHelp, PhysicalDiskFormat.SHEEPDOG)); | |
| 391 | 391 | } | |
| 392 | 392 | ||
| 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 | + | ||
| 393 | 408 | @Test | |
| 394 | 409 | public void testCheckAndRepair() throws LibvirtException { | |
| 395 | 410 | String filename = "/tmp/" + UUID.randomUUID() + ".qcow2"; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -28,6 +28,8 @@ | |||
| 28 | 28 | import java.util.ArrayList; | |
| 29 | 29 | import java.util.HashMap; | |
| 30 | 30 | import java.util.Map; | |
| 31 | + import java.util.Set; | ||
| 32 | + import java.util.concurrent.ConcurrentHashMap; | ||
| 31 | 33 | ||
| 32 | 34 | import javax.net.ssl.HostnameVerifier; | |
| 33 | 35 | import javax.net.ssl.SSLContext; | |
@@ -63,6 +65,7 @@ | |||
| 63 | 65 | import com.cloud.utils.exception.CloudRuntimeException; | |
| 64 | 66 | import com.fasterxml.jackson.core.JsonProcessingException; | |
| 65 | 67 | import com.fasterxml.jackson.core.type.TypeReference; | |
| 68 | + import com.fasterxml.jackson.databind.JsonNode; | ||
| 66 | 69 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 67 | 70 | import org.apache.logging.log4j.LogManager; | |
| 68 | 71 | import org.apache.logging.log4j.Logger; | |
@@ -87,6 +90,10 @@ public class FlashArrayAdapter implements ProviderAdapter { | |||
| 87 | 90 | private static final String API_LOGIN_VERSION_DEFAULT = "1.19"; | |
| 88 | 91 | private static final String API_VERSION_DEFAULT = "2.23"; | |
| 89 | 92 | ||
| 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 | + | ||
| 90 | 97 | static final ObjectMapper mapper = new ObjectMapper(); | |
| 91 | 98 | public String pod = null; | |
| 92 | 99 | public String hostgroup = null; | |
@@ -589,6 +596,91 @@ private String getAccessToken() { | |||
| 589 | 596 | return accessToken; | |
| 590 | 597 | } | |
| 591 | 598 | ||
| 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 | + | ||
| 592 | 684 | private synchronized void refreshSession(boolean force) { | |
| 593 | 685 | try { | |
| 594 | 686 | if (force || keyExpiration < System.currentTimeMillis()) { | |
@@ -663,9 +755,11 @@ private void login() { | |||
| 663 | 755 | } | |
| 664 | 756 | ||
| 665 | 757 | apiVersion = connectionDetails.get(FlashArrayAdapter.API_VERSION); | |
| 666 | - if (apiVersion == null) { | ||
| 758 | + boolean apiVersionExplicit = apiVersion != null; | ||
| 759 | + if (!apiVersionExplicit) { | ||
| 667 | 760 | apiVersion = queryParms.get(FlashArrayAdapter.API_VERSION); | |
| 668 | - if (apiVersion == null) { | ||
| 761 | + apiVersionExplicit = apiVersion != null; | ||
| 762 | + if (!apiVersionExplicit) { | ||
| 669 | 763 | apiVersion = API_VERSION_DEFAULT; | |
| 670 | 764 | } | |
| 671 | 765 | } | |
@@ -732,72 +826,66 @@ private void login() { | |||
| 732 | 826 | skipTlsValidation = true; | |
| 733 | 827 | } | |
| 734 | 828 | ||
| 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(); | ||
| 735 | 843 | CloseableHttpResponse response = null; | |
| 736 | 844 | 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); | ||
| 763 | 849 | } | |
| 764 | 850 | ||
| 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 | + } | ||
| 769 | 854 | ||
| 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) { | ||
| 772 | 861 | Header[] headers = response.getHeaders("x-auth-token"); | |
| 773 | 862 | if (headers == null || headers.length == 0) { | |
| 774 | 863 | 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"); | ||
| 776 | 865 | } | |
| 777 | 866 | accessToken = headers[0].getValue(); | |
| 778 | 867 | } else if (statusCode == 401 || statusCode == 403) { | |
| 779 | 868 | 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"); | ||
| 782 | 870 | } else { | |
| 783 | 871 | 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()); | ||
| 786 | 875 | } | |
| 787 | - | ||
| 788 | 876 | } 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); | ||
| 790 | 878 | } catch (UnsupportedOperationException e) { | |
| 791 | 879 | throw new CloudRuntimeException("Error processing login response from FlashArray [" + url + "]", e); | |
| 792 | 880 | } catch (IOException e) { | |
| 793 | 881 | throw new CloudRuntimeException("Error sending login request to FlashArray [" + url + "]", e); | |
| 794 | 882 | } finally { | |
| 795 | - try { | ||
| 796 | - if (response != null) { | ||
| 883 | + if (response != null) { | ||
| 884 | + try { | ||
| 797 | 885 | response.close(); | |
| 886 | + } catch (IOException e) { | ||
| 887 | + logger.debug("Error closing response from login attempt to FlashArray", e); | ||
| 798 | 888 | } | |
| 799 | - } catch (IOException e) { | ||
| 800 | - logger.debug("Error closing response from login attempt to FlashArray", e); | ||
| 801 | 889 | } | |
| 802 | 890 | } | |
| 803 | 891 | } | |
@@ -965,7 +1053,7 @@ private <T> T PATCH(String path, Object input, final TypeReference<T> type) { | |||
| 965 | 1053 | request.setEntity(new StringEntity(data)); | |
| 966 | 1054 | ||
| 967 | 1055 | CloseableHttpClient client = getClient(); | |
| 968 | - response = (CloseableHttpResponse) client.execute(request); | ||
| 1056 | + response = client.execute(request); | ||
| 969 | 1057 | ||
| 970 | 1058 | final int statusCode = response.getStatusLine().getStatusCode(); | |
| 971 | 1059 | if (statusCode == 200 || statusCode == 201) { | |
@@ -1020,7 +1108,7 @@ private <T> T GET(String path, final TypeReference<T> type) { | |||
| 1020 | 1108 | request.addHeader("X-auth-token", getAccessToken()); | |
| 1021 | 1109 | ||
| 1022 | 1110 | CloseableHttpClient client = getClient(); | |
| 1023 | - response = (CloseableHttpResponse) client.execute(request); | ||
| 1111 | + response = client.execute(request); | ||
| 1024 | 1112 | final int statusCode = response.getStatusLine().getStatusCode(); | |
| 1025 | 1113 | if (statusCode == 200) { | |
| 1026 | 1114 | try { | |
@@ -1062,7 +1150,7 @@ private void DELETE(String path) { | |||
| 1062 | 1150 | request.addHeader("X-auth-token", getAccessToken()); | |
| 1063 | 1151 | ||
| 1064 | 1152 | CloseableHttpClient client = getClient(); | |
| 1065 | - response = (CloseableHttpResponse) client.execute(request); | ||
| 1153 | + response = client.execute(request); | ||
| 1066 | 1154 | final int statusCode = response.getStatusLine().getStatusCode(); | |
| 1067 | 1155 | if (statusCode == 200 || statusCode == 404 || statusCode == 400) { | |
| 1068 | 1156 | // this means the volume was deleted successfully, or doesn't exist (effective | |
| Back | FazBrowse Home | New Git URL |
0 commit comments