Skip to content

Commit

Permalink
[WFLY-5603] Get data source information
Browse files Browse the repository at this point in the history
[WFLY-5603] Get data source information - negative tests by Martin Simka
  • Loading branch information
gaol committed Mar 15, 2018
1 parent 1ce89da commit 1ca9c89
Show file tree
Hide file tree
Showing 11 changed files with 545 additions and 9 deletions.
Expand Up @@ -908,4 +908,7 @@ public interface ConnectorLogger extends BasicLogger {
@LogMessage(level = ERROR) @LogMessage(level = ERROR)
@Message(id =113, value = "Unexcepted error during worker execution : %s") @Message(id =113, value = "Unexcepted error during worker execution : %s")
void unexceptedWorkerCompletionError(String errorMessage, @Cause Throwable t); void unexceptedWorkerCompletionError(String errorMessage, @Cause Throwable t);

@Message(id = 114, value = "Failed to load datasource class: %s")
IllegalStateException failedToLoadDataSourceClass(String clsName, @Cause Throwable t);
} }
Expand Up @@ -32,6 +32,7 @@
import org.jboss.as.controller.ObjectListAttributeDefinition; import org.jboss.as.controller.ObjectListAttributeDefinition;
import org.jboss.as.controller.ObjectTypeAttributeDefinition; import org.jboss.as.controller.ObjectTypeAttributeDefinition;
import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PrimitiveListAttributeDefinition;
import org.jboss.as.controller.PropertiesAttributeDefinition; import org.jboss.as.controller.PropertiesAttributeDefinition;
import org.jboss.as.controller.SimpleAttributeDefinition; import org.jboss.as.controller.SimpleAttributeDefinition;
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
Expand Down Expand Up @@ -880,6 +881,11 @@ public void validateResolvedParameter(String parameterName, ModelNode value) thr
.setAllowExpression(true) .setAllowExpression(true)
.build(); .build();


static final PrimitiveListAttributeDefinition DATASOURCE_CLASS_INFO = PrimitiveListAttributeDefinition.Builder.of("datasource-class-info", ModelType.OBJECT)
.setRequired(false)
.setStorageRuntime()
.build();

static final SimpleAttributeDefinition[] JDBC_DRIVER_ATTRIBUTES = { static final SimpleAttributeDefinition[] JDBC_DRIVER_ATTRIBUTES = {
DEPLOYMENT_NAME, DEPLOYMENT_NAME,
DRIVER_NAME, DRIVER_NAME,
Expand Down
Expand Up @@ -161,7 +161,7 @@ public class DataSourcesExtension implements Extension {
public static final String SUBSYSTEM_NAME = Constants.DATASOURCES; public static final String SUBSYSTEM_NAME = Constants.DATASOURCES;
private static final String RESOURCE_NAME = DataSourcesExtension.class.getPackage().getName() + ".LocalDescriptions"; private static final String RESOURCE_NAME = DataSourcesExtension.class.getPackage().getName() + ".LocalDescriptions";


static final ModelVersion CURRENT_MODEL_VERSION = ModelVersion.create(5, 0, 0); static final ModelVersion CURRENT_MODEL_VERSION = ModelVersion.create(6, 0, 0);


static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) { static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME); StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
Expand Down
@@ -0,0 +1,197 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2018, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.jboss.as.connector.subsystems.datasources;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import org.jboss.as.connector.logging.ConnectorLogger;
import org.jboss.as.connector.services.driver.InstalledDriver;
import org.jboss.as.connector.services.driver.registry.DriverRegistry;
import org.jboss.as.connector.util.ConnectorServices;
import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.server.Services;
import org.jboss.as.server.moduleservice.ServiceModuleLoader;
import org.jboss.dmr.ModelNode;
import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoadException;
import org.jboss.msc.service.ServiceRegistry;

/**
* Reads data-source and xa-data-source class information for a jdbc-driver.
*
* @author <a href="mailto:lgao@redhat.com">Lin Gao</a>
*/
@SuppressWarnings("deprecation")
// ModuleIdentifier is used in InstalledDriver, which is deprecated.
public class GetDataSourceClassInfoOperationHandler implements OperationStepHandler {

public static final GetDataSourceClassInfoOperationHandler INSTANCE = new GetDataSourceClassInfoOperationHandler();

private GetDataSourceClassInfoOperationHandler() {
}

@Override
public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException {

final String driverName = context.getCurrentAddressValue();
if (context.isNormalServer()) {
context.addStep(new OperationStepHandler() {

@Override
public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException {

ServiceRegistry registry = context.getServiceRegistry(false);
DriverRegistry driverRegistry = (DriverRegistry)registry.getRequiredService(ConnectorServices.JDBC_DRIVER_REGISTRY_SERVICE).getValue();
ServiceModuleLoader serviceModuleLoader = (ServiceModuleLoader)registry.getRequiredService(Services.JBOSS_SERVICE_MODULE_LOADER).getValue();
InstalledDriver driver = driverRegistry.getInstalledDriver(driverName);
if (driver == null) {
context.getResult().set(new ModelNode());
return;
}
ModelNode result = dsClsInfoNode(serviceModuleLoader, driver.getModuleName(), driver.getDataSourceClassName(),
driver.getXaDataSourceClassName());
context.getResult().set(result);
}

}, OperationContext.Stage.RUNTIME);
}
}

static ModelNode dsClsInfoNode(ServiceModuleLoader serviceModuleLoader, ModuleIdentifier mid, String dsClsName, String xaDSClsName)
throws OperationFailedException {
ModelNode result = new ModelNode();
if (dsClsName != null) {
ModelNode dsNode = new ModelNode();
dsNode.get(dsClsName).set(findPropsFromCls(serviceModuleLoader, mid, dsClsName));
result.add(dsNode);
}
if (xaDSClsName != null) {
ModelNode xaDSNode = new ModelNode();
xaDSNode.get(xaDSClsName).set(findPropsFromCls(serviceModuleLoader, mid, xaDSClsName));
result.add(xaDSNode);
}
return result;
}

private static ModelNode findPropsFromCls(ServiceModuleLoader serviceModuleLoader, ModuleIdentifier mid, String clsName) throws OperationFailedException {
Class<?> cls = null;
if (mid != null) {
try {
cls = Class.forName(clsName, true, serviceModuleLoader.loadModule(mid.toString()).getClassLoader());
} catch (ModuleLoadException | ClassNotFoundException e) {
throw ConnectorLogger.SUBSYSTEM_DATASOURCES_LOGGER.failedToLoadDataSourceClass(clsName, e);
}
}
if (cls == null) {
try {
cls = Class.forName(clsName);
} catch (ClassNotFoundException e) {
throw ConnectorLogger.SUBSYSTEM_DATASOURCES_LOGGER.failedToLoadDataSourceClass(clsName, e);
}
}
Map<String, Type> methodsMap = new TreeMap<>();
for (Method method : possiblePropsSetters(cls)) {
methodsMap.putIfAbsent(deCapitalize(method.getName().substring(3)), method.getParameterTypes()[0]);
}
final ModelNode result = new ModelNode();
for (Map.Entry<String, Type> prop: methodsMap.entrySet()) {
result.get(prop.getKey()).set(prop.getValue().getTypeName());
}
return result;
}

/**
* Check whether the types that JCA Injection knows.
*
* @see Injection.findMethod()
* @param clz the class
* @return whether it is understandable
*/
private static boolean isTypeMatched(Class<?> clz) {
if (clz.equals(String.class)) {
return true;
} else if (clz.equals(byte.class) || clz.equals(Byte.class)) {
return true;
} else if (clz.equals(short.class) || clz.equals(Short.class)) {
return true;
} else if (clz.equals(int.class) || clz.equals(Integer.class)) {
return true;
} else if (clz.equals(long.class) || clz.equals(Long.class)) {
return true;
} else if (clz.equals(float.class) || clz.equals(Float.class)) {
return true;
} else if (clz.equals(double.class) || clz.equals(Double.class)) {
return true;
} else if (clz.equals(boolean.class) || clz.equals(Boolean.class)) {
return true;
} else if (clz.equals(char.class) || clz.equals(Character.class)) {
return true;
} else if (clz.equals(InetAddress.class)) {
return true;
} else if (clz.equals(Class.class)) {
return true;
} else if (clz.equals(Properties.class)) {
return true;
}
return false;
}

private static String deCapitalize(String str) {
if (str.length() == 1) {
return str.toLowerCase(Locale.US);
}
if (str.equals(str.toUpperCase(Locale.US))) { // all uppercase, just return
return str;
}
return str.substring(0, 1).toLowerCase(Locale.US) + str.substring(1);
}

private static List<Method> possiblePropsSetters(Class<?> clz) {
List<Method> hits = new ArrayList<>();
while (!clz.equals(Object.class)) {
Method[] methods = clz.getMethods();
for (int i = 0; i < methods.length; i++) {
final Method method = methods[i];
if (!Modifier.isStatic(method.getModifiers())
&& method.getName().length() > 3
&& method.getName().startsWith("set")
&& method.getParameterCount() == 1
&& isTypeMatched(method.getParameterTypes()[0]))
hits.add(method);
}
clz = clz.getSuperclass();
}
return hits;
}
}
Expand Up @@ -25,6 +25,7 @@
*/ */
package org.jboss.as.connector.subsystems.datasources; package org.jboss.as.connector.subsystems.datasources;


import static org.jboss.as.connector.subsystems.datasources.Constants.DATASOURCE_CLASS_INFO;
import static org.jboss.as.connector.subsystems.datasources.Constants.DEPLOYMENT_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.DEPLOYMENT_NAME;
import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_CLASS_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_CLASS_NAME;
import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_MAJOR_VERSION; import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_MAJOR_VERSION;
Expand All @@ -34,6 +35,7 @@
import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_XA_DATASOURCE_CLASS_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_XA_DATASOURCE_CLASS_NAME;
import static org.jboss.as.connector.subsystems.datasources.Constants.JDBC_COMPLIANT; import static org.jboss.as.connector.subsystems.datasources.Constants.JDBC_COMPLIANT;
import static org.jboss.as.connector.subsystems.datasources.Constants.MODULE_SLOT; import static org.jboss.as.connector.subsystems.datasources.Constants.MODULE_SLOT;
import static org.jboss.as.connector.subsystems.datasources.GetDataSourceClassInfoOperationHandler.dsClsInfoNode;


import org.jboss.as.connector.services.driver.InstalledDriver; import org.jboss.as.connector.services.driver.InstalledDriver;
import org.jboss.as.connector.services.driver.registry.DriverRegistry; import org.jboss.as.connector.services.driver.registry.DriverRegistry;
Expand All @@ -43,8 +45,10 @@
import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.operations.validation.ParametersValidator; import org.jboss.as.controller.operations.validation.ParametersValidator;
import org.jboss.as.controller.operations.validation.StringLengthValidator; import org.jboss.as.controller.operations.validation.StringLengthValidator;
import org.jboss.as.server.Services;
import org.jboss.as.server.moduleservice.ServiceModuleLoader;
import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelNode;
import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceRegistry;


/** /**
* Reads the "installed-drivers" attribute. * Reads the "installed-drivers" attribute.
Expand Down Expand Up @@ -72,9 +76,9 @@ public void execute(final OperationContext context, final ModelNode operation) t
@Override @Override
public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException {


ServiceController<?> sc = context.getServiceRegistry(false).getRequiredService( ServiceRegistry registry = context.getServiceRegistry(false);
ConnectorServices.JDBC_DRIVER_REGISTRY_SERVICE); DriverRegistry driverRegistry = (DriverRegistry)registry.getRequiredService(ConnectorServices.JDBC_DRIVER_REGISTRY_SERVICE).getValue();
DriverRegistry driverRegistry = DriverRegistry.class.cast(sc.getValue()); ServiceModuleLoader serviceModuleLoader = (ServiceModuleLoader)registry.getRequiredService(Services.JBOSS_SERVICE_MODULE_LOADER).getValue();
ModelNode result = new ModelNode(); ModelNode result = new ModelNode();
InstalledDriver driver = driverRegistry.getInstalledDriver(name); InstalledDriver driver = driverRegistry.getInstalledDriver(name);
ModelNode driverNode = new ModelNode(); ModelNode driverNode = new ModelNode();
Expand All @@ -91,6 +95,8 @@ public void execute(final OperationContext context, final ModelNode operation) t
driverNode.get(DRIVER_XA_DATASOURCE_CLASS_NAME.getName()).set(driver.getXaDataSourceClassName()); driverNode.get(DRIVER_XA_DATASOURCE_CLASS_NAME.getName()).set(driver.getXaDataSourceClassName());


} }
driverNode.get(DATASOURCE_CLASS_INFO.getName()).set(
dsClsInfoNode(serviceModuleLoader, driver.getModuleName(), driver.getDataSourceClassName(), driver.getXaDataSourceClassName()));
driverNode.get(DRIVER_CLASS_NAME.getName()).set(driver.getDriverClassName()); driverNode.get(DRIVER_CLASS_NAME.getName()).set(driver.getDriverClassName());
driverNode.get(DRIVER_MAJOR_VERSION.getName()).set(driver.getMajorVersion()); driverNode.get(DRIVER_MAJOR_VERSION.getName()).set(driver.getMajorVersion());
driverNode.get(DRIVER_MINOR_VERSION.getName()).set(driver.getMinorVersion()); driverNode.get(DRIVER_MINOR_VERSION.getName()).set(driver.getMinorVersion());
Expand Down
Expand Up @@ -25,6 +25,7 @@
*/ */
package org.jboss.as.connector.subsystems.datasources; package org.jboss.as.connector.subsystems.datasources;


import static org.jboss.as.connector.subsystems.datasources.Constants.DATASOURCE_CLASS_INFO;
import static org.jboss.as.connector.subsystems.datasources.Constants.DEPLOYMENT_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.DEPLOYMENT_NAME;
import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_CLASS_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_CLASS_NAME;
import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_DATASOURCE_CLASS_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.DRIVER_DATASOURCE_CLASS_NAME;
Expand All @@ -36,6 +37,7 @@
import static org.jboss.as.connector.subsystems.datasources.Constants.JDBC_COMPLIANT; import static org.jboss.as.connector.subsystems.datasources.Constants.JDBC_COMPLIANT;
import static org.jboss.as.connector.subsystems.datasources.Constants.MODULE_SLOT; import static org.jboss.as.connector.subsystems.datasources.Constants.MODULE_SLOT;
import static org.jboss.as.connector.subsystems.datasources.Constants.PROFILE; import static org.jboss.as.connector.subsystems.datasources.Constants.PROFILE;
import static org.jboss.as.connector.subsystems.datasources.GetDataSourceClassInfoOperationHandler.dsClsInfoNode;


import org.jboss.as.connector.logging.ConnectorLogger; import org.jboss.as.connector.logging.ConnectorLogger;
import org.jboss.as.connector.services.driver.InstalledDriver; import org.jboss.as.connector.services.driver.InstalledDriver;
Expand All @@ -46,8 +48,10 @@
import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.registry.Resource; import org.jboss.as.controller.registry.Resource;
import org.jboss.as.server.Services;
import org.jboss.as.server.moduleservice.ServiceModuleLoader;
import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelNode;
import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceRegistry;


/** /**
* Reads the "installed-drivers" attribute. * Reads the "installed-drivers" attribute.
Expand All @@ -64,9 +68,9 @@ public void execute(OperationContext context, ModelNode operation) throws Operat
if (context.isNormalServer()) { if (context.isNormalServer()) {
context.addStep(new OperationStepHandler() { context.addStep(new OperationStepHandler() {
public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
ServiceController<?> sc = context.getServiceRegistry(false).getRequiredService( ServiceRegistry registry = context.getServiceRegistry(false);
ConnectorServices.JDBC_DRIVER_REGISTRY_SERVICE); DriverRegistry driverRegistry = (DriverRegistry)registry.getRequiredService(ConnectorServices.JDBC_DRIVER_REGISTRY_SERVICE).getValue();
DriverRegistry driverRegistry = DriverRegistry.class.cast(sc.getValue()); ServiceModuleLoader serviceModuleLoader = (ServiceModuleLoader)registry.getRequiredService(Services.JBOSS_SERVICE_MODULE_LOADER).getValue();
Resource rootNode = context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, false); Resource rootNode = context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, false);
ModelNode rootModel = rootNode.getModel(); ModelNode rootModel = rootNode.getModel();
String profile = rootModel.hasDefined("profile-name") ? rootModel.get("profile-name").asString() : null; String profile = rootModel.hasDefined("profile-name") ? rootModel.get("profile-name").asString() : null;
Expand All @@ -93,6 +97,8 @@ public void execute(OperationContext context, ModelNode operation) throws Operat
driver.getXaDataSourceClassName() != null ? driver.getXaDataSourceClassName() : ""); driver.getXaDataSourceClassName() != null ? driver.getXaDataSourceClassName() : "");


} }
driverNode.get(DATASOURCE_CLASS_INFO.getName()).set(
dsClsInfoNode(serviceModuleLoader, driver.getModuleName(), driver.getDataSourceClassName(), driver.getXaDataSourceClassName()));
driverNode.get(DRIVER_CLASS_NAME.getName()).set(driver.getDriverClassName()); driverNode.get(DRIVER_CLASS_NAME.getName()).set(driver.getDriverClassName());
driverNode.get(DRIVER_MAJOR_VERSION.getName()).set(driver.getMajorVersion()); driverNode.get(DRIVER_MAJOR_VERSION.getName()).set(driver.getMajorVersion());
driverNode.get(DRIVER_MINOR_VERSION.getName()).set(driver.getMinorVersion()); driverNode.get(DRIVER_MINOR_VERSION.getName()).set(driver.getMinorVersion());
Expand Down
Expand Up @@ -24,6 +24,7 @@


package org.jboss.as.connector.subsystems.datasources; package org.jboss.as.connector.subsystems.datasources;


import static org.jboss.as.connector.subsystems.datasources.Constants.DATASOURCE_CLASS_INFO;
import static org.jboss.as.connector.subsystems.datasources.Constants.JDBC_DRIVER_NAME; import static org.jboss.as.connector.subsystems.datasources.Constants.JDBC_DRIVER_NAME;


import java.util.List; import java.util.List;
Expand Down Expand Up @@ -60,6 +61,7 @@ public void registerAttributes(ManagementResourceRegistration resourceRegistrati
for (AttributeDefinition attribute : Constants.JDBC_DRIVER_ATTRIBUTES) { for (AttributeDefinition attribute : Constants.JDBC_DRIVER_ATTRIBUTES) {
resourceRegistration.registerReadOnlyAttribute(attribute, null); resourceRegistration.registerReadOnlyAttribute(attribute, null);
} }
resourceRegistration.registerMetric(DATASOURCE_CLASS_INFO, GetDataSourceClassInfoOperationHandler.INSTANCE);
} }


@Override @Override
Expand Down
Expand Up @@ -23,6 +23,7 @@ datasources.jdbc-driver.description=A service that makes a JDBC driver available
datasources.jdbc-driver.module=The name of the module that makes the JDBC driver available under the service type "java.sql.Driver" datasources.jdbc-driver.module=The name of the module that makes the JDBC driver available under the service type "java.sql.Driver"
datasources.jdbc-driver.remove=Remove a JDBC driver datasources.jdbc-driver.remove=Remove a JDBC driver


datasources.jdbc-driver.datasource-class-info=The available properties for the datasource-class, and xa-datasource-class for the jdbc-driver
datasources.jdbc-driver.driver-major-version=The driver's major version number datasources.jdbc-driver.driver-major-version=The driver's major version number
datasources.jdbc-driver.driver-minor-version=The driver's minor version number datasources.jdbc-driver.driver-minor-version=The driver's minor version number
datasources.jdbc-driver.driver-datasource-class-name=The fully qualified class name of the javax.sql.DataSource implementation datasources.jdbc-driver.driver-datasource-class-name=The fully qualified class name of the javax.sql.DataSource implementation
Expand Down
Expand Up @@ -133,6 +133,14 @@ You can easily query the same information through the CLI:
    "outcome" => "success",     "outcome" => "success",
    "result" => [{     "result" => [{
        "driver-name" => "h2",         "driver-name" => "h2",
"datasource-class-info" => [{"org.h2.jdbcx.JdbcDataSource" => {
"URL" => "java.lang.String",
"description" => "java.lang.String",
"loginTimeout" => "int",
"password" => "java.lang.String",
"url" => "java.lang.String",
"user" => "java.lang.String"
}}],
        "deployment-name" => undefined,         "deployment-name" => undefined,
        "driver-module-name" => "com.h2database.h2",         "driver-module-name" => "com.h2database.h2",
        "module-slot" => "main",         "module-slot" => "main",
Expand All @@ -145,6 +153,10 @@ You can easily query the same information through the CLI:
} }
---- ----


[NOTE]

`datasource-class-info` shows connection properties defined in the `(xa-)datasource-class`.

[TIP] [TIP]


Using the web console or the CLI greatly simplifies the deployment of Using the web console or the CLI greatly simplifies the deployment of
Expand Down

0 comments on commit 1ca9c89

Please sign in to comment.