This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 48ffa5dc0b Support multiple ceph monitors (#6792)
48ffa5dc0b is described below

commit 48ffa5dc0b9e6dcbc4d208245862050e8814f69b
Author: Wei Zhou <[email protected]>
AuthorDate: Fri Oct 21 10:37:30 2022 +0200

    Support multiple ceph monitors (#6792)
---
 .../kvm/resource/LibvirtDomainXMLParser.java       |   2 +-
 .../kvm/resource/LibvirtStoragePoolDef.java        |  12 +-
 .../kvm/resource/LibvirtStoragePoolXMLParser.java  |  13 +-
 .../hypervisor/kvm/resource/LibvirtVMDef.java      |  14 +-
 .../hypervisor/kvm/storage/KVMPhysicalDisk.java    |  28 +++-
 .../kvm/resource/LibvirtStoragePoolDefTest.java    |  32 +++++
 .../resource/LibvirtStoragePoolXMLParserTest.java  | 104 ++++++++++++++
 .../hypervisor/kvm/resource/LibvirtVMDefTest.java  |  66 +++++++++
 .../kvm/storage/KVMPhysicalDiskTest.java           |  20 +++
 .../CloudStackPrimaryDataStoreLifeCycleImpl.java   |  49 +++----
 ui/public/locales/en.json                          |   1 +
 ui/src/views/infra/AddPrimaryStorage.vue           |   5 +-
 utils/src/main/java/com/cloud/utils/UriUtils.java  |  84 +++++++++++
 .../test/java/com/cloud/utils/UriUtilsTest.java    | 154 ++++++++++++++++++++-
 14 files changed, 539 insertions(+), 45 deletions(-)

diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java
index 606115e6de..da9b2233c8 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java
@@ -83,7 +83,7 @@ public class LibvirtDomainXMLParser {
                     String protocol = getAttrValue("source", "protocol", disk);
                     String authUserName = getAttrValue("auth", "username", 
disk);
                     String poolUuid = getAttrValue("secret", "uuid", disk);
-                    String host = getAttrValue("host", "name", disk);
+                    String host = 
LibvirtStoragePoolXMLParser.getStorageHosts(disk);
                     int port = 0;
                     String xmlPort = getAttrValue("host", "port", disk);
                     if (StringUtils.isNotBlank(xmlPort)) {
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDef.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDef.java
index 1bdf2db8c4..f0ec29f41f 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDef.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDef.java
@@ -147,10 +147,14 @@ public class LibvirtStoragePoolDef {
         }
         if (_poolType == PoolType.RBD) {
             storagePoolBuilder.append("<source>\n");
-            if (_sourcePort > 0) {
-                storagePoolBuilder.append("<host name='" + _sourceHost + "' 
port='" + _sourcePort + "'/>\n");
-            } else {
-                storagePoolBuilder.append("<host name='" + _sourceHost + 
"'/>\n");
+            for (String sourceHost : _sourceHost.split(",")) {
+                storagePoolBuilder.append("<host name='");
+                storagePoolBuilder.append(sourceHost);
+                if (_sourcePort != 0) {
+                    storagePoolBuilder.append("' port='");
+                    storagePoolBuilder.append(_sourcePort);
+                }
+                storagePoolBuilder.append("'/>\n");
             }
 
             storagePoolBuilder.append("<name>" + _sourceDir + "</name>\n");
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParser.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParser.java
index 7f0bf8c0bc..d19c851d7d 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParser.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParser.java
@@ -18,6 +18,8 @@ package com.cloud.hypervisor.kvm.resource;
 
 import java.io.IOException;
 import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.ParserConfigurationException;
@@ -52,7 +54,7 @@ public class LibvirtStoragePoolXMLParser {
             String poolName = getTagValue("name", rootElement);
 
             Element source = 
(Element)rootElement.getElementsByTagName("source").item(0);
-            String host = getAttrValue("host", "name", source);
+            String host = getStorageHosts(source);
             String format = getAttrValue("format", "type", source);
 
             if (type.equalsIgnoreCase("rbd") || 
type.equalsIgnoreCase("powerflex")) {
@@ -123,4 +125,13 @@ public class LibvirtStoragePoolXMLParser {
         Element node = (Element)tagNode.item(0);
         return node.getAttribute(attr);
     }
+
+    protected static String getStorageHosts(Element parentElement) {
+        List<String> storageHosts = new ArrayList<>();
+        NodeList hosts = parentElement.getElementsByTagName("host");
+        for (int j = 0; j < hosts.getLength(); j++) {
+            storageHosts.add(((Element) hosts.item(j)).getAttribute("name"));
+        }
+        return StringUtils.join(storageHosts, ",");
+    }
 }
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
index a6fec3b60c..2c1e362fc6 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java
@@ -1093,13 +1093,15 @@ public class LibvirtVMDef {
                 diskBuilder.append(" protocol='" + _diskProtocol + "'");
                 diskBuilder.append(" name='" + _sourcePath + "'");
                 diskBuilder.append(">\n");
-                diskBuilder.append("<host name='");
-                diskBuilder.append(_sourceHost);
-                if (_sourcePort != 0) {
-                    diskBuilder.append("' port='");
-                    diskBuilder.append(_sourcePort);
+                for (String sourceHost : _sourceHost.split(",")) {
+                    diskBuilder.append("<host name='");
+                    diskBuilder.append(sourceHost.replace("[", 
"").replace("]", ""));
+                    if (_sourcePort != 0) {
+                        diskBuilder.append("' port='");
+                        diskBuilder.append(_sourcePort);
+                    }
+                    diskBuilder.append("'/>\n");
                 }
-                diskBuilder.append("'/>\n");
                 diskBuilder.append("</source>\n");
                 if (_authUserName != null) {
                     diskBuilder.append("<auth username='" + _authUserName + 
"'>\n");
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java
index 7de6230f33..c9abf39953 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDisk.java
@@ -18,6 +18,10 @@ package com.cloud.hypervisor.kvm.storage;
 
 import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
 import org.apache.cloudstack.utils.qemu.QemuObject;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
 
 public class KVMPhysicalDisk {
     private String path;
@@ -29,10 +33,7 @@ public class KVMPhysicalDisk {
         String rbdOpts;
 
         rbdOpts = "rbd:" + image;
-        rbdOpts += ":mon_host=" + monHost;
-        if (monPort > 0) {
-            rbdOpts += "\\:" + monPort;
-        }
+        rbdOpts += ":mon_host=" + composeOptionForMonHosts(monHost, monPort);
 
         if (authUserName == null) {
             rbdOpts += ":auth_supported=none";
@@ -48,6 +49,25 @@ public class KVMPhysicalDisk {
         return rbdOpts;
     }
 
+    private static String composeOptionForMonHosts(String monHost, int 
monPort) {
+        List<String> hosts = new ArrayList<>();
+        for (String host : monHost.split(",")) {
+            if (monPort > 0) {
+                hosts.add(replaceHostAddress(host) + "\\:" + monPort);
+            } else {
+                hosts.add(replaceHostAddress(host));
+            }
+        }
+        return StringUtils.join(hosts, "\\;");
+    }
+
+    private static String replaceHostAddress(String hostIp) {
+        if (hostIp != null && hostIp.startsWith("[") && hostIp.endsWith("]")) {
+            return hostIp.replaceAll("\\:", "\\\\:");
+        }
+        return hostIp;
+    }
+
     private PhysicalDiskFormat format;
     private long size;
     private long virtualSize;
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDefTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDefTest.java
index a1a43447e1..f5f3cc994e 100644
--- 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDefTest.java
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolDefTest.java
@@ -22,6 +22,7 @@ package com.cloud.hypervisor.kvm.resource;
 import junit.framework.TestCase;
 import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef.PoolType;
 import 
com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef.AuthenticationType;
+import org.junit.Test;
 
 public class LibvirtStoragePoolDefTest extends TestCase {
 
@@ -102,4 +103,35 @@ public class LibvirtStoragePoolDefTest extends TestCase {
 
         assertEquals(expectedXml, pool.toString());
     }
+
+    @Test
+    public void testRbdStoragePoolWithMultipleHostsIpv6() {
+        PoolType type = PoolType.RBD;
+        String name = "myRBDPool";
+        String uuid = "1583a25a-b192-436c-93e6-0ef60b198a32";
+        String host = "[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]";
+        int port = 3300;
+        String authUsername = "admin";
+        AuthenticationType auth = AuthenticationType.CEPH;
+        String dir  = "rbd";
+        String secretUuid = "28909c4f-314e-4db7-a6b3-5eccd9dcf973";
+
+        LibvirtStoragePoolDef pool = new LibvirtStoragePoolDef(type, name, 
uuid, host, port, dir, authUsername, auth, secretUuid);
+
+        String expected = "<pool type='rbd'>\n" +
+                "<name>myRBDPool</name>\n" +
+                "<uuid>1583a25a-b192-436c-93e6-0ef60b198a32</uuid>\n" +
+                "<source>\n" +
+                "<host name='[fc00:1234::1]' port='3300'/>\n" +
+                "<host name='[fc00:1234::2]' port='3300'/>\n" +
+                "<host name='[fc00:1234::3]' port='3300'/>\n" +
+                "<name>rbd</name>\n" +
+                "<auth username='admin' type='ceph'>\n" +
+                "<secret uuid='28909c4f-314e-4db7-a6b3-5eccd9dcf973'/>\n" +
+                "</auth>\n" +
+                "</source>\n" +
+                "</pool>\n";
+
+        assertEquals(expected, pool.toString());
+    }
 }
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParserTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParserTest.java
new file mode 100644
index 0000000000..90dfae019a
--- /dev/null
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtStoragePoolXMLParserTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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 com.cloud.hypervisor.kvm.resource;
+
+import junit.framework.TestCase;
+import org.junit.Assert;
+
+public class LibvirtStoragePoolXMLParserTest extends TestCase {
+
+    public void testParseNfsStoragePoolXML() {
+        String poolXML = "<pool type='netfs'>\n" +
+                "  <name>feff06b5-84b2-3258-b5f9-1953217295de</name>\n" +
+                "  <uuid>feff06b5-84b2-3258-b5f9-1953217295de</uuid>\n" +
+                "  <capacity unit='bytes'>111111111</capacity>\n" +
+                "  <allocation unit='bytes'>2222222</allocation>\n" +
+                "  <available unit='bytes'>3333333</available>\n" +
+                "  <source>\n" +
+                "    <host name='10.11.12.13'/>\n" +
+                "    <dir path='/mnt/primary1'/>\n" +
+                "    <format type='auto'/>\n" +
+                "  </source>\n" +
+                "  <target>\n" +
+                "    <path>/mnt/feff06b5-84b2-3258-b5f9-1953217295de</path>\n" 
+
+                "    <permissions>\n" +
+                "      <mode>0755</mode>\n" +
+                "      <owner>0</owner>\n" +
+                "      <group>0</group>\n" +
+                "    </permissions>\n" +
+                "  </target>\n" +
+                "</pool>";
+
+        LibvirtStoragePoolXMLParser parser = new LibvirtStoragePoolXMLParser();
+        LibvirtStoragePoolDef pool = parser.parseStoragePoolXML(poolXML);
+
+        Assert.assertEquals("10.11.12.13", pool.getSourceHost());
+    }
+
+    public void testParseRbdStoragePoolXMLWithMultipleHosts() {
+        String poolXML = "<pool type='rbd'>\n" +
+                "  <name>feff06b5-84b2-3258-b5f9-1953217295de</name>\n" +
+                "  <uuid>feff06b5-84b2-3258-b5f9-1953217295de</uuid>\n" +
+                "  <source>\n" +
+                "    <name>rbdpool</name>\n" +
+                "    <host name='10.11.12.13' port='6789'/>\n" +
+                "    <host name='10.11.12.14' port='6789'/>\n" +
+                "    <host name='10.11.12.15' port='6789'/>\n" +
+                "    <format type='auto'/>\n" +
+                "    <auth username='admin' type='ceph'>\n" +
+                "      <secret 
uuid='262f743a-3726-11ed-aaee-93e90b39d5c4'/>\n" +
+                "    </auth>\n" +
+                "  </source>\n" +
+                "</pool>";
+
+        LibvirtStoragePoolXMLParser parser = new LibvirtStoragePoolXMLParser();
+        LibvirtStoragePoolDef pool = parser.parseStoragePoolXML(poolXML);
+
+        Assert.assertEquals(LibvirtStoragePoolDef.PoolType.RBD, 
pool.getPoolType());
+        Assert.assertEquals(LibvirtStoragePoolDef.AuthenticationType.CEPH, 
pool.getAuthType());
+        Assert.assertEquals("10.11.12.13,10.11.12.14,10.11.12.15", 
pool.getSourceHost());
+        Assert.assertEquals(6789, pool.getSourcePort());
+    }
+
+    public void testParseRbdStoragePoolXMLWithMultipleHostsIpv6() {
+        String poolXML = "<pool type='rbd'>\n" +
+                "  <name>feff06b5-84b2-3258-b5f9-1953217295de</name>\n" +
+                "  <uuid>feff06b5-84b2-3258-b5f9-1953217295de</uuid>\n" +
+                "  <source>\n" +
+                "    <name>rbdpool</name>\n" +
+                "    <host name='[fc00:aa:bb:cc::1]' port='6789'/>\n" +
+                "    <host name='[fc00:aa:bb:cc::2]' port='6789'/>\n" +
+                "    <host name='[fc00:aa:bb:cc::3]' port='6789'/>\n" +
+                "    <format type='auto'/>\n" +
+                "    <auth username='admin' type='ceph'>\n" +
+                "      <secret 
uuid='262f743a-3726-11ed-aaee-93e90b39d5c4'/>\n" +
+                "    </auth>\n" +
+                "  </source>\n" +
+                "</pool>";
+
+        LibvirtStoragePoolXMLParser parser = new LibvirtStoragePoolXMLParser();
+        LibvirtStoragePoolDef pool = parser.parseStoragePoolXML(poolXML);
+
+        Assert.assertEquals(LibvirtStoragePoolDef.PoolType.RBD, 
pool.getPoolType());
+        Assert.assertEquals(LibvirtStoragePoolDef.AuthenticationType.CEPH, 
pool.getAuthType());
+        
Assert.assertEquals("[fc00:aa:bb:cc::1],[fc00:aa:bb:cc::2],[fc00:aa:bb:cc::3]", 
pool.getSourceHost());
+        Assert.assertEquals(6789, pool.getSourcePort());
+    }
+}
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
index 4eb464e4a6..9da1f5ea75 100644
--- 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java
@@ -238,6 +238,72 @@ public class LibvirtVMDefTest extends TestCase {
         assertEquals(disk.toString(), expectedXML);
     }
 
+    @Test
+    public void testDiskDefWithMultipleHosts() {
+        String path = "/mnt/primary1";
+        String host = "10.11.12.13,10.11.12.14,10.11.12.15";
+        int port = 3300;
+        String authUsername = "admin";
+        String uuid = "40b3f216-36b5-11ed-9357-9b4e21b0ed91";
+        int devId = 2;
+
+        DiskDef diskdef = new DiskDef();
+        diskdef.defNetworkBasedDisk(path, host, port, authUsername,
+                uuid, devId, DiskDef.DiskBus.VIRTIO, DiskDef.DiskProtocol.RBD, 
DiskDef.DiskFmtType.RAW);
+
+        assertEquals(path, diskdef.getDiskPath());
+        assertEquals(DiskDef.DiskType.NETWORK, diskdef.getDiskType());
+        assertEquals(DiskDef.DiskFmtType.RAW, diskdef.getDiskFormatType());
+
+        String expected = "<disk  device='disk' type='network'>\n" +
+                "<driver name='qemu' type='raw' cache='none' />\n" +
+                "<source  protocol='rbd' name='/mnt/primary1'>\n" +
+                "<host name='10.11.12.13' port='3300'/>\n" +
+                "<host name='10.11.12.14' port='3300'/>\n" +
+                "<host name='10.11.12.15' port='3300'/>\n" +
+                "</source>\n" +
+                "<auth username='admin'>\n" +
+                "<secret type='ceph' 
uuid='40b3f216-36b5-11ed-9357-9b4e21b0ed91'/>\n" +
+                "</auth>\n" +
+                "<target dev='vdc' bus='virtio'/>\n" +
+                "</disk>\n";
+
+        assertEquals(expected, diskdef.toString());
+    }
+
+    @Test
+    public void testDiskDefWithMultipleHostsIpv6() {
+        String path = "/mnt/primary1";
+        String host = "[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]";
+        int port = 3300;
+        String authUsername = "admin";
+        String uuid = "40b3f216-36b5-11ed-9357-9b4e21b0ed91";
+        int devId = 2;
+
+        DiskDef diskdef = new DiskDef();
+        diskdef.defNetworkBasedDisk(path, host, port, authUsername,
+                uuid, devId, DiskDef.DiskBus.VIRTIO, DiskDef.DiskProtocol.RBD, 
DiskDef.DiskFmtType.RAW);
+
+        assertEquals(path, diskdef.getDiskPath());
+        assertEquals(DiskDef.DiskType.NETWORK, diskdef.getDiskType());
+        assertEquals(DiskDef.DiskFmtType.RAW, diskdef.getDiskFormatType());
+
+        String expected = "<disk  device='disk' type='network'>\n" +
+                "<driver name='qemu' type='raw' cache='none' />\n" +
+                "<source  protocol='rbd' name='/mnt/primary1'>\n" +
+                "<host name='fc00:1234::1' port='3300'/>\n" +
+                "<host name='fc00:1234::2' port='3300'/>\n" +
+                "<host name='fc00:1234::3' port='3300'/>\n" +
+                "</source>\n" +
+                "<auth username='admin'>\n" +
+                "<secret type='ceph' 
uuid='40b3f216-36b5-11ed-9357-9b4e21b0ed91'/>\n" +
+                "</auth>\n" +
+                "<target dev='vdc' bus='virtio'/>\n" +
+                "</disk>\n";
+
+        assertEquals(expected, diskdef.toString());
+    }
+
     @Test
     public void testDiskDefWithBurst() {
         String filePath = "/var/lib/libvirt/images/disk.qcow2";
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDiskTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDiskTest.java
index cf39dceb1a..684a0e9daf 100644
--- 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDiskTest.java
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/KVMPhysicalDiskTest.java
@@ -28,6 +28,26 @@ public class KVMPhysicalDiskTest extends TestCase {
                      
"rbd:volume1:mon_host=ceph-monitor\\:8000:auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30");
     }
 
+    public void testRBDStringBuilder2() {
+        String monHosts = "ceph-monitor1,ceph-monitor2,ceph-monitor3";
+        int monPort = 3300;
+        String expected = "rbd:volume1:" +
+                
"mon_host=ceph-monitor1\\:3300\\;ceph-monitor2\\:3300\\;ceph-monitor3\\:3300:" +
+                
"auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30";
+        String actualResult = KVMPhysicalDisk.RBDStringBuilder(monHosts, 
monPort, "admin", "supersecret", "volume1");
+        assertEquals(expected, actualResult);
+    }
+
+    public void testRBDStringBuilder3() {
+        String monHosts = "[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]";
+        int monPort = 3300;
+        String expected = "rbd:volume1:" +
+                
"mon_host=[fc00\\:1234\\:\\:1]\\:3300\\;[fc00\\:1234\\:\\:2]\\:3300\\;[fc00\\:1234\\:\\:3]\\:3300:"
 +
+                
"auth_supported=cephx:id=admin:key=supersecret:rbd_default_format=2:client_mount_timeout=30";
+        String actualResult = KVMPhysicalDisk.RBDStringBuilder(monHosts, 
monPort, "admin", "supersecret", "volume1");
+        assertEquals(expected, actualResult);
+    }
+
     public void testAttributes() {
         String name = "3bc186e0-6c29-45bf-b2b0-ddef6f91f5ef";
         String path = "/" + name;
diff --git 
a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
 
b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
index f39b17076e..213e562055 100644
--- 
a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
+++ 
b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/lifecycle/CloudStackPrimaryDataStoreLifeCycleImpl.java
@@ -138,38 +138,35 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl 
implements PrimaryDataStore
 
         PrimaryDataStoreParameters parameters = new 
PrimaryDataStoreParameters();
 
-        URI uri = null;
+        UriUtils.UriInfo uriInfo = UriUtils.getUriInfo(url);
+
+        String scheme = uriInfo.getScheme();
+        String storageHost = uriInfo.getStorageHost();
+        String storagePath = uriInfo.getStoragePath();
         try {
-            uri = new URI(UriUtils.encodeURIComponent(url));
-            if (uri.getScheme() == null) {
+            if (scheme == null) {
                 throw new InvalidParameterValueException("scheme is null " + 
url + ", add nfs:// (or cifs://) as a prefix");
-            } else if (uri.getScheme().equalsIgnoreCase("nfs")) {
-                String uriHost = uri.getHost();
-                String uriPath = uri.getPath();
-                if (uriHost == null || uriPath == null || 
uriHost.trim().isEmpty() || uriPath.trim().isEmpty()) {
+            } else if (scheme.equalsIgnoreCase("nfs")) {
+                if (storageHost == null || storagePath == null || 
storageHost.trim().isEmpty() || storagePath.trim().isEmpty()) {
                     throw new InvalidParameterValueException("host or path is 
null, should be nfs://hostname/path");
                 }
-            } else if (uri.getScheme().equalsIgnoreCase("cifs")) {
+            } else if (scheme.equalsIgnoreCase("cifs")) {
                 // Don't validate against a URI encoded URI.
                 URI cifsUri = new URI(url);
                 String warnMsg = 
UriUtils.getCifsUriParametersProblems(cifsUri);
                 if (warnMsg != null) {
                     throw new InvalidParameterValueException(warnMsg);
                 }
-            } else if (uri.getScheme().equalsIgnoreCase("sharedMountPoint")) {
-                String uriPath = uri.getPath();
-                if (uriPath == null) {
+            } else if (scheme.equalsIgnoreCase("sharedMountPoint")) {
+                if (storagePath == null) {
                     throw new InvalidParameterValueException("host or path is 
null, should be sharedmountpoint://localhost/path");
                 }
-            } else if (uri.getScheme().equalsIgnoreCase("rbd")) {
-                String uriPath = uri.getPath();
-                if (uriPath == null) {
+            } else if (scheme.equalsIgnoreCase("rbd")) {
+                if (storagePath == null) {
                     throw new InvalidParameterValueException("host or path is 
null, should be rbd://hostname/pool");
                 }
-            } else if (uri.getScheme().equalsIgnoreCase("gluster")) {
-                String uriHost = uri.getHost();
-                String uriPath = uri.getPath();
-                if (uriHost == null || uriPath == null || 
uriHost.trim().isEmpty() || uriPath.trim().isEmpty()) {
+            } else if (scheme.equalsIgnoreCase("gluster")) {
+                if (storageHost == null || storagePath == null || 
storageHost.trim().isEmpty() || storagePath.trim().isEmpty()) {
                     throw new InvalidParameterValueException("host or path is 
null, should be gluster://hostname/volume");
                 }
             }
@@ -183,24 +180,22 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl 
implements PrimaryDataStore
         parameters.setTags(tags);
         parameters.setDetails(details);
 
-        String scheme = uri.getScheme();
-        String storageHost = uri.getHost();
         String hostPath = null;
         try {
-          hostPath = URLDecoder.decode(uri.getPath(), "UTF-8");
+            hostPath = URLDecoder.decode(storagePath, "UTF-8");
         } catch (UnsupportedEncodingException e) {
             s_logger.error("[ignored] we are on a platform not supporting 
\"UTF-8\"!?!", e);
         }
         if (hostPath == null) { // if decoding fails, use getPath() anyway
-            hostPath = uri.getPath();
+            hostPath = storagePath;
         }
         Object localStorage = dsInfos.get("localStorage");
         if (localStorage != null) {
             hostPath = hostPath.replaceFirst("/", "");
             hostPath = hostPath.replace("+", " ");
         }
-        String userInfo = uri.getUserInfo();
-        int port = uri.getPort();
+        String userInfo = uriInfo.getUserInfo();
+        int port = uriInfo.getPort();
         if (s_logger.isDebugEnabled()) {
             s_logger.debug("createPool Params @ scheme - " + scheme + " 
storageHost - " + storageHost + " hostPath - " + hostPath + " port - " + port);
         }
@@ -317,8 +312,8 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl 
implements PrimaryDataStore
                 parameters.setPort(0);
                 parameters.setPath(hostPath);
             } else {
-                s_logger.warn("Unable to figure out the scheme for URI: " + 
uri);
-                throw new IllegalArgumentException("Unable to figure out the 
scheme for URI: " + uri);
+                s_logger.warn("Unable to figure out the scheme for URI: " + 
uriInfo);
+                throw new IllegalArgumentException("Unable to figure out the 
scheme for URI: " + uriInfo);
             }
         }
 
@@ -326,7 +321,7 @@ public class CloudStackPrimaryDataStoreLifeCycleImpl 
implements PrimaryDataStore
             List<StoragePoolVO> pools = 
primaryDataStoreDao.listPoolByHostPath(storageHost, hostPath);
             if (!pools.isEmpty() && 
!scheme.equalsIgnoreCase("sharedmountpoint")) {
                 Long oldPodId = pools.get(0).getPodId();
-                throw new CloudRuntimeException("Storage pool " + uri + " 
already in use by another pod (id=" + oldPodId + ")");
+                throw new CloudRuntimeException("Storage pool " + uriInfo + " 
already in use by another pod (id=" + oldPodId + ")");
             }
         }
 
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index dce9040c33..babd453baa 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -1363,6 +1363,7 @@
 "label.quota.value": "Quota value",
 "label.quota_enforce": "Enforce Quota",
 "label.rados.monitor": "RADOS monitor",
+"label.rados.monitor.description": "The RADOS monitor(s). If there are 
multiple monitors, they are separated by comma. For example, 
\"192.168.0.1,192.168.0.2,192.168.0.3\", \"mon1, mon2, mon3\". IPv6 addresses 
must include square brackets, for example,  
\"[fc00:1234::1],[fc00:1234::2],[fc00:1234::3]\".",
 "label.rados.pool": "RADOS pool",
 "label.rados.secret": "RADOS secret",
 "label.rados.user": "RADOS user",
diff --git a/ui/src/views/infra/AddPrimaryStorage.vue 
b/ui/src/views/infra/AddPrimaryStorage.vue
index 9e628dd287..b6f7922d79 100644
--- a/ui/src/views/infra/AddPrimaryStorage.vue
+++ b/ui/src/views/infra/AddPrimaryStorage.vue
@@ -284,7 +284,10 @@
           </a-form-item>
         </div>
         <div v-if="form.protocol === 'RBD'">
-          <a-form-item name="radosmonitor" ref="radosmonitor" 
:label="$t('label.rados.monitor')">
+          <a-form-item name="radosmonitor" ref="radosmonitor">
+            <template #label>
+              <tooltip-label :title="$t('label.rados.monitor')" 
:tooltip="$t('label.rados.monitor.description')"/>
+            </template>
             <a-input v-model:value="form.radosmonitor" 
:placeholder="$t('label.rados.monitor')" />
           </a-form-item>
           <a-form-item name="radospool" ref="radospool" 
:label="$t('label.rados.pool')">
diff --git a/utils/src/main/java/com/cloud/utils/UriUtils.java 
b/utils/src/main/java/com/cloud/utils/UriUtils.java
index fd7adf38d5..0dbf8d3f35 100644
--- a/utils/src/main/java/com/cloud/utils/UriUtils.java
+++ b/utils/src/main/java/com/cloud/utils/UriUtils.java
@@ -639,4 +639,88 @@ public class UriUtils {
         expandedVlans.add(Integer.parseInt(parts[1]));
         return expandedVlans;
     }
+
+    public static class UriInfo {
+        String scheme;
+        String storageHost;
+        String storagePath;
+        String userInfo;
+        int port = -1;
+
+        public UriInfo() {
+        }
+
+        public UriInfo(String scheme, String storageHost, String storagePath, 
String userInfo, int port) {
+            this.scheme = scheme;
+            this.storageHost = storageHost;
+            this.storagePath = storagePath;
+            this.userInfo = userInfo;
+            this.port = port;
+        }
+
+        public String getScheme() {
+            return scheme;
+        }
+
+        public String getStorageHost() {
+            return storageHost;
+        }
+
+        public String getStoragePath() {
+            return storagePath;
+        }
+
+        public String getUserInfo() {
+            return userInfo;
+        }
+
+        public int getPort() {
+            return port;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%s://%s%s%s%s", scheme,
+                    userInfo == null ? "" : userInfo + "@",
+                    storageHost,
+                    port == -1 ? "" : ":" + port,
+                    storagePath == null ? "" : storagePath);
+        }
+    }
+
+    public static UriInfo getUriInfo(String url) {
+        try {
+            if (url == null) {
+                return new UriInfo();
+            }
+            if (url.startsWith("rbd://")) {
+                return getRbdUrlInfo(url);
+            }
+            URI uri = new URI(UriUtils.encodeURIComponent(url));
+            return new UriInfo(uri.getScheme(), uri.getHost(), uri.getPath(), 
uri.getUserInfo(), uri.getPort());
+        } catch (URISyntaxException e) {
+            throw new CloudRuntimeException(url + " is not a valid uri");
+        }
+    }
+
+    private static UriInfo getRbdUrlInfo(String url) {
+        int secondSlash = StringUtils.ordinalIndexOf(url, "/", 2);
+        int thirdSlash = StringUtils.ordinalIndexOf(url, "/", 3);
+        int firstAt = StringUtils.indexOf(url, "@");
+        int lastColon = StringUtils.lastIndexOf(url,":");
+        int lastSquareBracket = StringUtils.lastIndexOf(url,"]");
+        int startOfHost = Math.max(secondSlash, firstAt) + 1;
+        int endOfHost = lastColon < startOfHost ? (thirdSlash > 0 ? thirdSlash 
: url.length() + 1) :
+                (lastSquareBracket > lastColon ? lastSquareBracket + 1 : 
lastColon);
+        String storageHosts = StringUtils.substring(url, startOfHost, 
endOfHost);
+        String firstHost = storageHosts.split(",")[0];
+        String strBeforeHosts = StringUtils.substring(url, 0, startOfHost);
+        String strAfterHosts = StringUtils.substring(url, endOfHost);
+        try {
+            URI uri = new URI(UriUtils.encodeURIComponent(strBeforeHosts + 
firstHost + strAfterHosts));
+            return new UriInfo(uri.getScheme(), storageHosts, uri.getPath(), 
uri.getUserInfo(), uri.getPort());
+        } catch (URISyntaxException e) {
+            throw new CloudRuntimeException(url + " is not a valid uri for 
RBD");
+        }
+    }
 }
diff --git a/utils/src/test/java/com/cloud/utils/UriUtilsTest.java 
b/utils/src/test/java/com/cloud/utils/UriUtilsTest.java
index b8d951db34..db02e607d6 100644
--- a/utils/src/test/java/com/cloud/utils/UriUtilsTest.java
+++ b/utils/src/test/java/com/cloud/utils/UriUtilsTest.java
@@ -19,7 +19,7 @@
 
 package com.cloud.utils;
 
-import junit.framework.Assert;
+import org.junit.Assert;
 import org.junit.Test;
 
 import java.util.Arrays;
@@ -101,4 +101,156 @@ public class UriUtilsTest {
         Assert.assertFalse(UriUtils.checkVlanUriOverlap("10,22,111", "12"));
         Assert.assertFalse(UriUtils.checkVlanUriOverlap("100-200", 
"30-40,50,201-250"));
     }
+
+    private void testGetUriInfoInternal(String url, String host) {
+        UriUtils.UriInfo uriInfo = UriUtils.getUriInfo(url);
+
+        Assert.assertEquals(host, uriInfo.getStorageHost());
+        Assert.assertEquals(url, uriInfo.toString());
+    }
+
+    @Test
+    public void testGetRbdUriInfo() {
+        String host = "10.11.12.13";
+
+        String url0 = 
String.format("rbd://user:password@%s:3300/pool/volume2", host);
+        String url1 = String.format("rbd://user:password@%s:3300/pool", host);
+        String url2 = String.format("rbd://user:password@%s/pool", host);
+        String url3 = String.format("rbd://%s:3300/pool", host);
+        String url4 = String.format("rbd://%s/pool", host);
+        String url5 = String.format("rbd://user:password@%s", host);
+        String url6 = String.format("rbd://%s:3300", host);
+        String url7 = String.format("rbd://%s", host);
+        String url8 = String.format("rbd://user@%s", host);
+
+        testGetUriInfoInternal(url0, host);
+        testGetUriInfoInternal(url1, host);
+        testGetUriInfoInternal(url2, host);
+        testGetUriInfoInternal(url3, host);
+        testGetUriInfoInternal(url4, host);
+        testGetUriInfoInternal(url5, host);
+        testGetUriInfoInternal(url6, host);
+        testGetUriInfoInternal(url7, host);
+        testGetUriInfoInternal(url8, host);
+    }
+
+    @Test
+    public void testGetRbdUriInfoSingleIpv6() {
+        String host = "[fc00:aa:bb:cc::1]";
+
+        String url0 = 
String.format("rbd://user:password@%s:3300/pool/volume2", host);
+        String url1 = String.format("rbd://user:password@%s:3300/pool", host);
+        String url2 = String.format("rbd://user:password@%s/pool", host);
+        String url3 = String.format("rbd://%s:3300/pool", host);
+        String url4 = String.format("rbd://%s/pool", host);
+        String url5 = String.format("rbd://user:password@%s", host);
+        String url6 = String.format("rbd://%s:3300", host);
+        String url7 = String.format("rbd://%s", host);
+        String url8 = String.format("rbd://user@%s", host);
+
+        testGetUriInfoInternal(url0, host);
+        testGetUriInfoInternal(url1, host);
+        testGetUriInfoInternal(url2, host);
+        testGetUriInfoInternal(url3, host);
+        testGetUriInfoInternal(url4, host);
+        testGetUriInfoInternal(url5, host);
+        testGetUriInfoInternal(url6, host);
+        testGetUriInfoInternal(url7, host);
+        testGetUriInfoInternal(url8, host);
+    }
+
+    @Test
+    public void testGetRbdUriInfoMultipleIpv6() {
+        String host1 = "[fc00:aa:bb:cc::1]";
+        String host2 = "[fc00:aa:bb:cc::2]";
+        String host3 = "[fc00:aa:bb:cc::3]";
+
+        String url0 = 
String.format("rbd://user:password@%s,%s,%s:3300/pool/volume2", host1, host2, 
host3);
+        String url1 = String.format("rbd://user:password@%s,%s,%s:3300/pool", 
host1, host2, host3);
+        String url2 = String.format("rbd://user:password@%s,%s,%s/pool", 
host1, host2, host3);
+        String url3 = String.format("rbd://%s,%s,%s:3300/pool", host1, host2, 
host3);
+        String url4 = String.format("rbd://%s,%s,%s/pool", host1, host2, 
host3);
+        String url5 = String.format("rbd://user:password@%s,%s,%s", host1, 
host2, host3);
+        String url6 = String.format("rbd://%s,%s,%s:3300", host1, host2, 
host3);
+        String url7 = String.format("rbd://%s,%s,%s", host1, host2, host3);
+        String url8 = String.format("rbd://user@%s,%s,%s", host1, host2, 
host3);
+
+        String host = String.format("%s,%s,%s", host1, host2, host3);
+
+        testGetUriInfoInternal(url0, host);
+        testGetUriInfoInternal(url1, host);
+        testGetUriInfoInternal(url2, host);
+        testGetUriInfoInternal(url3, host);
+        testGetUriInfoInternal(url4, host);
+        testGetUriInfoInternal(url5, host);
+        testGetUriInfoInternal(url6, host);
+        testGetUriInfoInternal(url7, host);
+        testGetUriInfoInternal(url8, host);
+    }
+
+    @Test
+    public void testGetUriInfo() {
+        String host = "10.11.12.13";
+
+        String url0 = 
String.format("nfs://user:password@%s:3300/pool/volume2", host);
+        String url1 = String.format("cifs://user:password@%s:3300/pool", host);
+        String url2 = String.format("file://user:password@%s/pool", host);
+        String url3 = String.format("sharedMountPoint://%s:3300/pool", host);
+        String url4 = String.format("clvm://%s/pool", host);
+        String url5 = String.format("PreSetup://user@%s", host);
+        String url6 = String.format("DatastoreCluster://%s:3300", host);
+        String url7 = String.format("iscsi://%s", host);
+        String url8 = String.format("iso://user@%s:3300/pool/volume2", host);
+        String url9 = String.format("vmfs://user@%s:3300/pool", host);
+        String url10 = String.format("ocfs2://user@%s/pool", host);
+        String url11 = String.format("gluster://%s:3300/pool", host);
+        String url12 = 
String.format("rbd://user:password@%s:3300/pool/volume2", host);
+
+        testGetUriInfoInternal(url0, host);
+        testGetUriInfoInternal(url1, host);
+        testGetUriInfoInternal(url2, host);
+        testGetUriInfoInternal(url3, host);
+        testGetUriInfoInternal(url4, host);
+        testGetUriInfoInternal(url5, host);
+        testGetUriInfoInternal(url6, host);
+        testGetUriInfoInternal(url7, host);
+        testGetUriInfoInternal(url8, host);
+        testGetUriInfoInternal(url9, host);
+        testGetUriInfoInternal(url10, host);
+        testGetUriInfoInternal(url11, host);
+        testGetUriInfoInternal(url12, host);
+    }
+
+    @Test
+    public void testGetUriInfoIpv6() {
+        String host = "[fc00:aa:bb:cc::1]";
+
+        String url0 = 
String.format("nfs://user:password@%s:3300/pool/volume2", host);
+        String url1 = String.format("cifs://user:password@%s:3300/pool", host);
+        String url2 = String.format("file://user:password@%s/pool", host);
+        String url3 = String.format("sharedMountPoint://%s:3300/pool", host);
+        String url4 = String.format("clvm://%s/pool", host);
+        String url5 = String.format("PreSetup://user@%s", host);
+        String url6 = String.format("DatastoreCluster://%s:3300", host);
+        String url7 = String.format("iscsi://%s", host);
+        String url8 = String.format("iso://user@%s:3300/pool/volume2", host);
+        String url9 = String.format("vmfs://user@%s:3300/pool", host);
+        String url10 = String.format("ocfs2://user@%s/pool", host);
+        String url11 = String.format("gluster://%s:3300/pool", host);
+        String url12 = 
String.format("rbd://user:password@%s:3300/pool/volume2", host);
+
+        testGetUriInfoInternal(url0, host);
+        testGetUriInfoInternal(url1, host);
+        testGetUriInfoInternal(url2, host);
+        testGetUriInfoInternal(url3, host);
+        testGetUriInfoInternal(url4, host);
+        testGetUriInfoInternal(url5, host);
+        testGetUriInfoInternal(url6, host);
+        testGetUriInfoInternal(url7, host);
+        testGetUriInfoInternal(url8, host);
+        testGetUriInfoInternal(url9, host);
+        testGetUriInfoInternal(url10, host);
+        testGetUriInfoInternal(url11, host);
+        testGetUriInfoInternal(url12, host);
+    }
 }


Reply via email to