diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index f8830ce6f87d..a26fd40705a6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -32,6 +32,7 @@ public class ApiConstants { public static final String ALLOCATED_DATE = "allocateddate"; public static final String ALLOCATED_ONLY = "allocatedonly"; public static final String ALLOCATED_TIME = "allocated"; + public static final String ALLOWED_ROLE_TYPES = "allowedroletypes"; public static final String ALLOW_USER_FORCE_STOP_VM = "allowuserforcestopvm"; public static final String ANNOTATION = "annotation"; public static final String API_KEY = "apikey"; @@ -198,7 +199,7 @@ public class ApiConstants { public static final String END_IPV6 = "endipv6"; public static final String END_PORT = "endport"; public static final String ENTRY_POINT = "entrypoint"; - public static final String ENTRY_POINT_SYNC = "entrypointsync"; + public static final String ENTRY_POINT_READY = "entrypointready"; public static final String ENTRY_TIME = "entrytime"; public static final String ERROR_MESSAGE = "errormessage"; public static final String EVENT_ID = "eventid"; @@ -559,7 +560,9 @@ public class ApiConstants { public static final String USER_SECRET_KEY = "usersecretkey"; public static final String USE_VIRTUAL_NETWORK = "usevirtualnetwork"; public static final String UPDATE_IN_SEQUENCE = "updateinsequence"; + public static final String VALIDATION_FORMAT = "validationformat"; public static final String VALUE = "value"; + public static final String VALUE_OPTIONS = "valueoptions"; public static final String VIRTUAL_MACHINE_ID = "virtualmachineid"; public static final String VIRTUAL_MACHINE_IDS = "virtualmachineids"; public static final String VIRTUAL_MACHINE_NAME = "virtualmachinename"; @@ -682,7 +685,6 @@ public class ApiConstants { public static final String PROJECT_ROLE_NAME = "projectrolename"; public static final String ROLE_TYPE = "roletype"; public static final String ROLE_NAME = "rolename"; - public static final String ROLES = "roles"; public static final String PERMISSION = "permission"; public static final String RULE = "rule"; public static final String RULES = "rules"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java index 7d527c39c262..d627f8077dc1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionParameterResponse.java @@ -35,24 +35,24 @@ public class ExtensionCustomActionParameterResponse extends BaseResponse { @Param(description = "Type of the parameter") private String type; - @SerializedName(ApiConstants.FORMAT) - @Param(description = "Format for value of the parameter. Available for specific types") - private String format; + @SerializedName(ApiConstants.VALIDATION_FORMAT) + @Param(description = "Validation format for value of the parameter. Available for specific types") + private String validationFormat; - @SerializedName(ApiConstants.OPTIONS) - @Param(description = "Options for value of the parameter") - private List options; + @SerializedName(ApiConstants.VALUE_OPTIONS) + @Param(description = "Comma-separated list of options for value of the parameter") + private List valueOptions; @SerializedName(ApiConstants.REQUIRED) @Param(description = "Whether the parameter is required or not") private Boolean required; - public ExtensionCustomActionParameterResponse(String name, String type, String format, List options, - boolean required) { + public ExtensionCustomActionParameterResponse(String name, String type, String validationFormat, List valueOptions, + boolean required) { this.name = name; this.type = type; - this.format = format; - this.options = options; + this.validationFormat = validationFormat; + this.valueOptions = valueOptions; this.required = required; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java index e27e9699863e..83ad61e16315 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionCustomActionResponse.java @@ -34,32 +34,32 @@ public class ExtensionCustomActionResponse extends BaseResponse { @SerializedName(ApiConstants.ID) - @Param(description = "ID of the extension custom action") + @Param(description = "ID of the custom action") private String id; @SerializedName(ApiConstants.NAME) - @Param(description = "Name of the extension custom action") + @Param(description = "Name of the custom action") private String name; @SerializedName(ApiConstants.DESCRIPTION) - @Param(description = "Description of the extension custom action") + @Param(description = "Description of the custom action") private String description; @SerializedName(ApiConstants.EXTENSION_ID) - @Param(description = "ID of the extension that this extension custom action belongs to") + @Param(description = "ID of the extension that this custom action belongs to") private String extensionId; @SerializedName(ApiConstants.EXTENSION_NAME) - @Param(description = "Name of the extension that this extension custom action belongs to") + @Param(description = "Name of the extension that this custom action belongs to") private String extensionName; @SerializedName(ApiConstants.RESOURCE_TYPE) @Param(description = "Resource type for which the action is available") private String resourceType; - @SerializedName(ApiConstants.ROLES) - @Param(description = "List of roles associated with the extension custom action") - private List roles; + @SerializedName(ApiConstants.ALLOWED_ROLE_TYPES) + @Param(description = "List of role types allowed for the custom action") + private List allowedRoleTypes; @SerializedName(ApiConstants.SUCCESS_MESSAGE) @Param(description = "Message that will be used on successful execution of the action") @@ -69,12 +69,16 @@ public class ExtensionCustomActionResponse extends BaseResponse { @Param(description = "Message that will be used on failure during execution of the action") private String errorMessage; + @SerializedName(ApiConstants.TIMEOUT) + @Param(description = "Specifies the timeout in seconds to wait for the action to complete before failing") + private Integer timeout; + @SerializedName(ApiConstants.ENABLED) - @Param(description = "Whether the extension custom action is enabled or not") + @Param(description = "Whether the custom action is enabled or not") private Boolean enabled; @SerializedName(ApiConstants.DETAILS) - @Param(description = "Details of the extension custom action") + @Param(description = "Details of the custom action") private Map details; @SerializedName(ApiConstants.PARAMETERS) @@ -82,7 +86,7 @@ public class ExtensionCustomActionResponse extends BaseResponse { private Set parameters; @SerializedName(ApiConstants.CREATED) - @Param(description = "Creation timestamp of the extension custom action") + @Param(description = "Creation timestamp of the custom action") private Date created; public ExtensionCustomActionResponse(String id, String name, String description) { @@ -131,12 +135,12 @@ public void setResourceType(String resourceType) { this.resourceType = resourceType; } - public List getRoles() { - return roles; + public List getAllowedRoleTypes() { + return allowedRoleTypes; } - public void setRoles(List roles) { - this.roles = roles; + public void setAllowedRoleTypes(List allowedRoleTypes) { + this.allowedRoleTypes = allowedRoleTypes; } public void setSuccessMessage(String successMessage) { @@ -147,6 +151,10 @@ public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + public void setEnabled(Boolean enabled) { this.enabled = enabled; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java index cd716f1c0529..077715d6f962 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ExtensionResponse.java @@ -51,9 +51,9 @@ public class ExtensionResponse extends BaseResponse { @Param(description = "The path of the entry point fo the extension") private String entryPoint; - @SerializedName(ApiConstants.ENTRY_POINT_SYNC) - @Param(description = "True if the extension entry point is in sync across management servers") - private Boolean entryPointSync; + @SerializedName(ApiConstants.ENTRY_POINT_READY) + @Param(description = "True if the extension entry point is in ready state across management servers") + private Boolean entryPointReady; @SerializedName(ApiConstants.IS_USER_DEFINED) @Param(description = "True if the extension is added by admin") @@ -90,8 +90,8 @@ public void setEntryPoint(String entryPoint) { this.entryPoint = entryPoint; } - public void setEntryPointSync(Boolean entryPointSync) { - this.entryPointSync = entryPointSync; + public void setEntryPointReady(Boolean entryPointReady) { + this.entryPointReady = entryPointReady; } public void setUserDefined(Boolean userDefined) { diff --git a/api/src/main/java/org/apache/cloudstack/extension/Extension.java b/api/src/main/java/org/apache/cloudstack/extension/Extension.java index ef40dbc42a74..af2d92ebe663 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/Extension.java +++ b/api/src/main/java/org/apache/cloudstack/extension/Extension.java @@ -33,7 +33,7 @@ enum State { String getDescription(); Type getType(); String getRelativeEntryPoint(); - boolean isEntryPointSync(); + boolean isEntryPointReady(); boolean isUserDefined(); State getState(); Date getCreated(); diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java index 0d0934ebe231..5591dab86439 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionCustomAction.java @@ -27,6 +27,7 @@ import java.util.UUID; import java.util.stream.Collectors; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.Identity; import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; @@ -69,12 +70,14 @@ public Class getAssociatedClass() { ResourceType getResourceType(); - Integer getRoles(); + Integer getAllowedRoleTypes(); String getSuccessMessage(); String getErrorMessage(); + int getTimeout(); + boolean isEnabled(); Date getCreated(); @@ -130,22 +133,22 @@ public Type getBaseType() { private final String name; private final Type type; - private final ValidationFormat format; - private List options; + private final ValidationFormat validationFormat; + private final List valueOptions; private final boolean required; - public Parameter(String name, Type type, ValidationFormat format, List options, boolean required) { + public Parameter(String name, Type type, ValidationFormat validationFormat, List valueOptions, boolean required) { this.name = name; this.type = type; - this.format = format; - this.options = options; + this.validationFormat = validationFormat; + this.valueOptions = valueOptions; this.required = required; } /** * Parses a CSV string into a list of validated options. */ - private static List parseOptions(String name, String csv, Type parsedType, ValidationFormat parsedFormat) { + private static List parseValueOptions(String name, String csv, Type parsedType, ValidationFormat parsedFormat) { if (StringUtils.isBlank(csv)) { return null; } @@ -157,8 +160,8 @@ private static List parseOptions(String name, String csv, Type parsedTyp case STRING: if (parsedFormat != null && parsedFormat != ValidationFormat.NONE) { for (String value : values) { - if (!isValidateStringValue(value, parsedFormat)) { - throw new InvalidParameterException(String.format("Invalid options with format: %s for parameter: %s", parsedFormat.name(), name)); + if (!isValidStringValue(value, parsedFormat)) { + throw new InvalidParameterException(String.format("Invalid value options with validation format: %s for parameter: %s", parsedFormat.name(), name)); } } } @@ -169,7 +172,7 @@ private static List parseOptions(String name, String csv, Type parsedTyp .map(v -> parseNumber(v, parsedFormat)) .collect(Collectors.toList()); } catch (NumberFormatException ignored) { - throw new InvalidParameterException(String.format("Invalid options with format: %s for parameter: %s", parsedFormat.name(), name)); + throw new InvalidParameterException(String.format("Invalid value options with validation format: %s for parameter: %s", parsedFormat.name(), name)); } default: throw new InvalidParameterException(String.format("Options not supported for type: %s for parameter: %s", parsedType, name)); @@ -183,8 +186,8 @@ private static Object parseNumber(String value, ValidationFormat parsedFormat) { return Integer.parseInt(value); } - private static boolean isValidateStringValue(String value, ValidationFormat format) { - switch (format) { + private static boolean isValidStringValue(String value, ValidationFormat validationFormat ) { + switch (validationFormat) { case NONE: return true; case UUID: @@ -211,11 +214,11 @@ private static boolean isValidateStringValue(String value, ValidationFormat form } public static Parameter fromMap(Map map) throws InvalidParameterException { - final String name = map.get("name"); - final String typeStr = map.get("type"); - final String formatStr = map.get("format"); - final String required = map.get("required"); - final String optionsStr = map.get("options"); + final String name = map.get(ApiConstants.NAME); + final String typeStr = map.get(ApiConstants.TYPE); + final String validationFormatStr = map.get(ApiConstants.VALIDATION_FORMAT); + final String required = map.get(ApiConstants.REQUIRED); + final String valueOptionsStr = map.get(ApiConstants.VALUE_OPTIONS); if (StringUtils.isBlank(name)) { throw new InvalidParameterValueException("Invalid parameter specified with empty name"); } @@ -227,13 +230,13 @@ public static Parameter fromMap(Map map) throws InvalidParameter throw new InvalidParameterValueException(String.format("Invalid type: %s specified for parameter: %s", typeStr, name)); } - ValidationFormat parsedFormat = EnumUtils.getEnumIgnoreCase(ValidationFormat.class, formatStr, ValidationFormat.NONE); + ValidationFormat parsedFormat = EnumUtils.getEnumIgnoreCase(ValidationFormat.class, validationFormatStr, ValidationFormat.NONE); if (!ValidationFormat.NONE.equals(parsedFormat) && parsedFormat.getBaseType() != parsedType) { throw new InvalidParameterValueException( - String.format("Invalid format: %s specified for type: %s", parsedFormat.name(), parsedType.name())); + String.format("Invalid validation format: %s specified for type: %s", parsedFormat.name(), parsedType.name())); } - List options = parseOptions(name, optionsStr, parsedType, parsedFormat); - return new Parameter(name, parsedType, parsedFormat, options, Boolean.parseBoolean(required)); + List valueOptions = parseValueOptions(name, valueOptionsStr, parsedType, parsedFormat); + return new Parameter(name, parsedType, parsedFormat, valueOptions, Boolean.parseBoolean(required)); } public String getName() { @@ -244,12 +247,12 @@ public Type getType() { return type; } - public ValidationFormat getFormat() { - return format; + public ValidationFormat getValidationFormat() { + return validationFormat; } - public List getOptions() { - return options; + public List getValueOptions() { + return valueOptions; } public boolean isRequired() { @@ -259,7 +262,7 @@ public boolean isRequired() { @Override public String toString() { return String.format("Parameter %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, - "name", "type", "required")); + ApiConstants.NAME, ApiConstants.TYPE, ApiConstants.REQUIRED)); } public static String toJsonFromList(List parameters) { @@ -272,7 +275,7 @@ public static List toListFromJson(String json) { } private void validateValueInOptions(Object value) { - if (CollectionUtils.isNotEmpty(options) && !options.contains(value)) { + if (CollectionUtils.isNotEmpty(valueOptions) && !valueOptions.contains(value)) { throw new InvalidParameterException(); } } @@ -288,11 +291,11 @@ public Object validatedValue(String value) { case DATE: return DateUtil.parseTZDateString(value); case NUMBER: - Object obj = parseNumber(value, format); + Object obj = parseNumber(value, validationFormat); validateValueInOptions(obj); return obj; default: - if (!isValidateStringValue(value, format)) { + if (!isValidStringValue(value, validationFormat)) { throw new IllegalArgumentException(); } validateValueInOptions(value); @@ -338,39 +341,39 @@ static class ParameterDeserializer implements JsonDeserializer { @Override public Parameter deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject obj = json.getAsJsonObject(); - String name = obj.get("name").getAsString(); - String typeStr = obj.get("type").getAsString(); - String formatStr = obj.has("format") ? obj.get("format").getAsString() : null; - boolean required = obj.has("required") && obj.get("required").getAsBoolean(); + String name = obj.get(ApiConstants.NAME).getAsString(); + String typeStr = obj.get(ApiConstants.TYPE).getAsString(); + String validationFormatStr = obj.has(ApiConstants.VALIDATION_FORMAT) ? obj.get(ApiConstants.VALIDATION_FORMAT).getAsString() : null; + boolean required = obj.has(ApiConstants.REQUIRED) && obj.get(ApiConstants.REQUIRED).getAsBoolean(); Parameter.Type typeEnum = Parameter.Type.valueOf(typeStr); - Parameter.ValidationFormat formatEnum = (formatStr != null) - ? Parameter.ValidationFormat.valueOf(formatStr) + Parameter.ValidationFormat validationFormatEnum = (validationFormatStr != null) + ? Parameter.ValidationFormat.valueOf(validationFormatStr) : Parameter.ValidationFormat.NONE; - List options = null; - if (obj.has("options") && obj.get("options").isJsonArray()) { - JsonArray optionsArray = obj.getAsJsonArray("options"); - options = new ArrayList<>(); - for (JsonElement el : optionsArray) { + List valueOptions = null; + if (obj.has(ApiConstants.VALUE_OPTIONS) && obj.get(ApiConstants.VALUE_OPTIONS).isJsonArray()) { + JsonArray valueOptionsArray = obj.getAsJsonArray(ApiConstants.VALUE_OPTIONS); + valueOptions = new ArrayList<>(); + for (JsonElement el : valueOptionsArray) { switch (typeEnum) { case STRING: - options.add(el.getAsString()); + valueOptions.add(el.getAsString()); break; case NUMBER: - if (formatEnum == Parameter.ValidationFormat.DECIMAL) { - options.add(el.getAsFloat()); + if (validationFormatEnum == Parameter.ValidationFormat.DECIMAL) { + valueOptions.add(el.getAsFloat()); } else { - options.add(el.getAsInt()); + valueOptions.add(el.getAsInt()); } break; default: - throw new JsonParseException("Unsupported type for options"); + throw new JsonParseException("Unsupported type for value options"); } } } - return new Parameter(name, typeEnum, formatEnum, options, required); + return new Parameter(name, typeEnum, validationFormatEnum, valueOptions, required); } } } diff --git a/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java b/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java index 554cfcf8ae39..811e6f0ed9b1 100644 --- a/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java +++ b/engine/components-api/src/main/java/com/cloud/hypervisor/ExternalProvisioner.java @@ -36,17 +36,6 @@ import com.cloud.utils.component.Manager; public interface ExternalProvisioner extends Manager { - /** - * Returns the unique name of the provider - * @return returns provider name - */ - String getName(); - - /** - * Returns description about the provider - * @return returns description - */ - String getDescription(); String getExtensionEntryPoint(String relativeEntryPoint); @@ -56,6 +45,8 @@ public interface ExternalProvisioner extends Manager { void cleanupExtensionEntryPoint(String extensionName, String extensionRelativeEntryPoint); + void cleanupExtensionPayloads(String extensionName, int olderThanDays, boolean cleanupDirectory); + PrepareExternalProvisioningAnswer prepareExternalProvisioning(String hostGuid, String extensionName, String extensionRelativeEntryPoint, PrepareExternalProvisioningCommand cmd); StartAnswer startInstance(String hostGuid, String extensionName, String extensionRelativeEntryPoint, StartCommand cmd); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql index 0c403e7482f2..07530c5710db 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42010to42100.sql @@ -177,8 +177,8 @@ CREATE TABLE IF NOT EXISTS `cloud`.`extension` ( `description` varchar(4096), `type` varchar(255) NOT NULL COMMENT 'Type of the extension: Orchestrator, etc', `relative_entry_point` varchar(2048) NOT NULL COMMENT 'Path of entry point for the extension relative to the root extensions directory', - `entry_point_sync` int unsigned DEFAULT '0' COMMENT 'True if the extension entry point is in sync across management servers', - `is_user_defined` int unsigned DEFAULT '0' COMMENT 'True if the extension is added by admin', + `entry_point_ready` tinyint(1) DEFAULT '0' COMMENT 'True if the extension entry point is in ready state across management servers', + `is_user_defined` tinyint(1) DEFAULT '0' COMMENT 'True if the extension is added by admin', `state` char(32) NOT NULL COMMENT 'State of the extension - Enabled or Disabled', `created` datetime NOT NULL, `removed` datetime DEFAULT NULL, @@ -227,10 +227,11 @@ CREATE TABLE IF NOT EXISTS `cloud`.`extension_custom_action` ( `description` varchar(4096), `extension_id` bigint(20) unsigned NOT NULL, `resource_type` varchar(255), - `roles_list` varchar(255) DEFAULT NULL, + `allowed_role_types` int unsigned NOT NULL DEFAULT '1', `success_message` varchar(4096), `error_message` varchar(4096), `enabled` boolean DEFAULT true, + `timeout` int unsigned NOT NULL DEFAULT '3' COMMENT 'The timeout in seconds to wait for the action to complete before failing', `created` datetime NOT NULL, `removed` datetime DEFAULT NULL, PRIMARY KEY (`id`), diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java index 70cb56f82cdc..7e8d49a0f2b4 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/AddCustomActionCmd.java @@ -48,13 +48,13 @@ public class AddCustomActionCmd extends BaseCmd { ExtensionsManager extensionsManager; @Parameter(name = ApiConstants.EXTENSION_ID, type = CommandType.UUID, required = true, - entityType = ExtensionResponse.class, description = "the extension id used to call the custom action") + entityType = ExtensionResponse.class, description = "The ID of the extension to associate the action with") private Long extensionId; - @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "the name of the command") + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "Name of the action") private String name; - @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the command") + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Description of the action") private String description; @Parameter(name = ApiConstants.RESOURCE_TYPE, @@ -62,11 +62,11 @@ public class AddCustomActionCmd extends BaseCmd { description = "Resource type for which the action is available") private String resourceType; - @Parameter(name = ApiConstants.ROLES, + @Parameter(name = ApiConstants.ALLOWED_ROLE_TYPES, type = CommandType.LIST, collectionType = CommandType.STRING, - description = "The list of allowed role types") - private List rolesList; + description = "List of role types allowed for the action") + private List allowedRoleTypes; @Parameter(name = ApiConstants.PARAMETERS, type = CommandType.MAP, description = "Parameters mapping for the action using keys - name, type, required. " + @@ -77,16 +77,21 @@ public class AddCustomActionCmd extends BaseCmd { @Parameter(name = ApiConstants.SUCCESS_MESSAGE, type = CommandType.STRING, description = "Success message that will be used on successful execution of the action. " + - "Name of the action and and extension can be used in the - actionName, extensionName. " - + "Example: Successfully complete {{actionName}} for {{extensionName") + "Name of the action, extension, resource can be used as - actionName, extensionName, resourceName. " + + "Example: Successfully complete {{actionName}} for {{resourceName}} with {{extensionName}}") protected String successMessage; @Parameter(name = ApiConstants.ERROR_MESSAGE, type = CommandType.STRING, description = "Error message that will be used on failure during execution of the action. " + - "Name of the action and and extension can be used in the - actionName, extensionName. " - + "Example: Failed to complete {{actionName}} for {{extensionName") + "Name of the action, extension, resource can be used as - actionName, extensionName, resourceName. " + + "Example: Failed to complete {{actionName}} for {{resourceName}} with {{extensionName}}") protected String errorMessage; + @Parameter(name = ApiConstants.TIMEOUT, + type = CommandType.INTEGER, + description = "Specifies the timeout in seconds to wait for the action to complete before failing. Default value is 3 seconds") + private Integer timeout; + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "Whether the action is enabled or not. Default is disabled.") @@ -118,12 +123,8 @@ public String getResourceType() { return resourceType; } - public List getRolesList() { - return rolesList; - } - - public boolean isEnabled() { - return Boolean.TRUE.equals(enabled); + public List getAllowedRoleTypes() { + return allowedRoleTypes; } public Map getParametersMap() { @@ -138,6 +139,14 @@ public String getErrorMessage() { return errorMessage; } + public Integer getTimeout() { + return timeout; + } + + public boolean isEnabled() { + return Boolean.TRUE.equals(enabled); + } + public Map getDetails() { return convertDetailsToMap(details); } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java index 094b39e82d58..bb03be00c5d5 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/api/UpdateCustomActionCmd.java @@ -61,11 +61,11 @@ public class UpdateCustomActionCmd extends BaseCmd { description = "Type of the resource for actions") private String resourceType; - @Parameter(name = ApiConstants.ROLES, + @Parameter(name = ApiConstants.ALLOWED_ROLE_TYPES, type = CommandType.LIST, collectionType = CommandType.STRING, - description = "The list of allowed role types") - private List roles; + description = "List of role types allowed for the action") + private List allowedRoleTypes; @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, @@ -98,6 +98,11 @@ public class UpdateCustomActionCmd extends BaseCmd { + "Example: Failed to complete {{actionName}} for {{extensionName") protected String errorMessage; + @Parameter(name = ApiConstants.TIMEOUT, + type = CommandType.INTEGER, + description = "Specifies the timeout in seconds to wait for the action to complete before failing. Default value is 3 seconds") + private Integer timeout; + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using format details[i].keyname=keyvalue. " @@ -127,12 +132,8 @@ public String getResourceType() { return resourceType; } - public Boolean isEnabled() { - return enabled; - } - - public List getRoles() { - return roles; + public List getAllowedRoleTypes() { + return allowedRoleTypes; } public Map getParametersMap() { @@ -151,6 +152,14 @@ public String getErrorMessage() { return errorMessage; } + public Integer getTimeout() { + return timeout; + } + + public Boolean isEnabled() { + return enabled; + } + public Map getDetails() { return convertDetailsToMap(details); } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionEntryPointCommand.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionFilesCommand.java similarity index 84% rename from framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionEntryPointCommand.java rename to framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionFilesCommand.java index 0919cffdc30d..ba542d52e85d 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionEntryPointCommand.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/command/CleanupExtensionFilesCommand.java @@ -19,9 +19,9 @@ import org.apache.cloudstack.extension.Extension; -public class CleanupExtensionEntryPointCommand extends ExtensionServerActionBaseCommand { +public class CleanupExtensionFilesCommand extends ExtensionServerActionBaseCommand { - public CleanupExtensionEntryPointCommand(long msId, Extension extension) { + public CleanupExtensionFilesCommand(long msId, Extension extension) { super(msId, extension); } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java index 3c36a7214023..36bcee2b0c64 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExtensionDao.java @@ -16,10 +16,13 @@ // under the License. package org.apache.cloudstack.framework.extensions.dao; +import java.util.List; + import com.cloud.utils.db.GenericDao; import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; public interface ExtensionDao extends GenericDao { ExtensionVO findByName(String name); + List listAllEnabled(); } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExternalOrchestratorDaoImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExternalOrchestratorDaoImpl.java index 5aa5aaa96b99..b974eeec0b59 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExternalOrchestratorDaoImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/dao/ExternalOrchestratorDaoImpl.java @@ -17,9 +17,13 @@ package org.apache.cloudstack.framework.extensions.dao; +import java.util.List; + import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; + +import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.framework.extensions.vo.ExtensionVO; public class ExternalOrchestratorDaoImpl extends GenericDaoBase implements ExtensionDao { @@ -30,6 +34,7 @@ public ExternalOrchestratorDaoImpl() { AllFieldSearch = createSearchBuilder(); AllFieldSearch.and("name", AllFieldSearch.entity().getName(), SearchCriteria.Op.EQ); AllFieldSearch.and("type", AllFieldSearch.entity().getType(), SearchCriteria.Op.EQ); + AllFieldSearch.and("state", AllFieldSearch.entity().getState(), SearchCriteria.Op.EQ); AllFieldSearch.done(); } @@ -40,4 +45,11 @@ public ExtensionVO findByName(String name) { return findOneBy(sc); } + + @Override + public List listAllEnabled() { + SearchCriteria sc = AllFieldSearch.create(); + sc.setParameters("state", Extension.State.Enabled); + return listBy(sc); + } } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java index 5eee2a87511d..b340ab897d15 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java @@ -52,7 +52,7 @@ public interface ExtensionsManager extends Manager { Extension createExtension(CreateExtensionCmd cmd); - void prepareExtensionEntryPointAcrossServers(Extension extension); + boolean prepareExtensionEntryPointAcrossServers(Extension extension); List listExtensions(ListExtensionsCmd cmd); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java index 0e14183bdcf2..1adcb638b136 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java @@ -20,6 +20,8 @@ package org.apache.cloudstack.framework.extensions.manager; import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.file.Path; import java.nio.file.Paths; import java.security.InvalidParameterException; @@ -64,7 +66,7 @@ import org.apache.cloudstack.framework.extensions.api.UnregisterExtensionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateCustomActionCmd; import org.apache.cloudstack.framework.extensions.api.UpdateExtensionCmd; -import org.apache.cloudstack.framework.extensions.command.CleanupExtensionEntryPointCommand; +import org.apache.cloudstack.framework.extensions.command.CleanupExtensionFilesCommand; import org.apache.cloudstack.framework.extensions.command.ExtensionServerActionBaseCommand; import org.apache.cloudstack.framework.extensions.command.GetExtensionEntryPointChecksumCommand; import org.apache.cloudstack.framework.extensions.command.PrepareExtensionEntryPointCommand; @@ -254,18 +256,19 @@ protected Pair prepareExtensionEntryPointOnCurrentServer(String return new Pair<>(true, null); } - protected boolean cleanupExtensionEntryPointOnMSPeer(Extension extension, ManagementServerHostVO msHost) { + protected boolean cleanupExtensionFilesOnMSPeer(Extension extension, ManagementServerHostVO msHost) { final String msPeer = Long.toString(msHost.getMsid()); logger.debug("Sending cleanup extension entry-point for {} command to MS: {}", extension, msPeer); final Command[] commands = new Command[1]; - commands[0] = new CleanupExtensionEntryPointCommand(ManagementServerNode.getManagementServerId(), extension); + commands[0] = new CleanupExtensionFilesCommand(ManagementServerNode.getManagementServerId(), extension); String answersStr = clusterManager.execute(msPeer, 0L, GsonHelper.getGson().toJson(commands), true); return getResultFromAnswersString(answersStr, extension, msHost, "cleanup entry-point").first(); } - protected Pair cleanupExtensionEntryPointOnCurrentServer(String name, String relativeEntryPoint) { + protected Pair cleanupExtensionFilesOnCurrentServer(String name, String relativeEntryPoint) { try { externalProvisioner.cleanupExtensionEntryPoint(name, relativeEntryPoint); + externalProvisioner.cleanupExtensionPayloads(name, 0, true); } catch (CloudRuntimeException e) { logger.error("Failed to cleanup entry-point files for Extension [name: {}, relativeEntryPoint: {}] on this server", name, relativeEntryPoint, e); @@ -274,16 +277,16 @@ protected Pair cleanupExtensionEntryPointOnCurrentServer(String return new Pair<>(true, null); } - protected void cleanupExtensionEntryPointAcrossServers(Extension extension) { + protected void cleanupExtensionFilesAcrossServers(Extension extension) { boolean cleanup = true; List msHosts = managementServerHostDao.listBy(ManagementServerHost.State.Up); for (ManagementServerHostVO msHost : msHosts) { if (msHost.getMsid() == ManagementServerNode.getManagementServerId()) { - cleanup = cleanup && cleanupExtensionEntryPointOnCurrentServer(extension.getName(), + cleanup = cleanup && cleanupExtensionFilesOnCurrentServer(extension.getName(), extension.getRelativeEntryPoint()).first(); continue; } - cleanup = cleanup && cleanupExtensionEntryPointOnMSPeer(extension, msHost); + cleanup = cleanup && cleanupExtensionFilesOnMSPeer(extension, msHost); } if (!cleanup) { throw new CloudRuntimeException("Extension is deleted but its entry-point files are not cleaned up across servers"); @@ -343,12 +346,26 @@ protected Extension getExtensionFromResource(ExtensionCustomAction.ResourceType return extensionDao.findById(mapVO.getExtensionId()); } - protected String getActionMessage(boolean success, ExtensionCustomAction action, Extension extension) { + protected String getActionMessage(boolean success, ExtensionCustomAction action, Extension extension, + ExtensionCustomAction.ResourceType resourceType, Object resource) { String msg = success ? action.getSuccessMessage() : action.getErrorMessage(); if (StringUtils.isNotBlank(msg)) { Map values = new HashMap<>(); values.put("actionName", action.getName()); values.put("extensionName", extension.getName()); + if (msg.contains("{{resourceName}}")) { + String resourceName = resourceType.name(); + try { + Method getNameMethod = resource.getClass().getMethod("getName"); + Object result = getNameMethod.invoke(resource); + if (result instanceof String) { + resourceName = (String) result; + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.trace("Failed to get name for given resource of type: {}", resourceType, e); + } + values.put("resourceName", resourceName); + } String result = msg; for (Map.Entry entry : values.entrySet()) { result = result.replace("{{" + entry.getKey() + "}}", entry.getValue()); @@ -370,6 +387,29 @@ protected Map getFilteredExternalDetails(Map det Map.Entry::getValue )); } + protected void sendExtensionEntryPointOutOfSyncAlert(Extension extension) { + String msg = String.format("Entry-point for %s are out of sync across management servers", + extension); + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_USERVM, 0L, 0L, msg, msg); + } + + protected void updateExtensionEntryPointReady(Extension extension, boolean ready) { + if (!ready) { + sendExtensionEntryPointOutOfSyncAlert(extension); + } + if (extension.isEntryPointReady() == ready) { + return; + } + ExtensionVO extensionVO = extensionDao.createForUpdate(extension.getId()); + extensionVO.setEntryPointReady(ready); + extensionDao.update(extension.getId(), extensionVO); + } + + protected void disableExtension(long extensionId) { + ExtensionVO extensionVO = extensionDao.createForUpdate(extensionId); + extensionVO.setState(Extension.State.Disabled); + extensionDao.update(extensionId, extensionVO); + } @Override @ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension") @@ -417,27 +457,34 @@ public Extension createExtension(CreateExtensionCmd cmd) { CallContext.current().setEventResourceId(extension.getId()); return extension; }); - prepareExtensionEntryPointAcrossServers(extensionVO); + if (Extension.State.Enabled.equals(extensionVO.getState()) && + !prepareExtensionEntryPointAcrossServers(extensionVO)) { + disableExtension(extensionVO.getId()); + throw new CloudRuntimeException(String.format( + "Failed to enable extension: %s as it entry-point is not ready", + extensionVO.getName())); + } return extensionVO; } @Override - public void prepareExtensionEntryPointAcrossServers(Extension extension) { - boolean sync = true; + public boolean prepareExtensionEntryPointAcrossServers(Extension extension) { + boolean prepared = true; List msHosts = managementServerHostDao.listBy(ManagementServerHost.State.Up); for (ManagementServerHostVO msHost : msHosts) { if (msHost.getMsid() == ManagementServerNode.getManagementServerId()) { - sync = sync && prepareExtensionEntryPointOnCurrentServer(extension.getName(), extension.isUserDefined(), + prepared = prepared && prepareExtensionEntryPointOnCurrentServer(extension.getName(), extension.isUserDefined(), extension.getRelativeEntryPoint()).first(); continue; } - sync = sync && prepareExtensionEntryPointOnMSPeer(extension, msHost); + prepared = prepared && prepareExtensionEntryPointOnMSPeer(extension, msHost); } - if (extension.isEntryPointSync() != sync) { + if (extension.isEntryPointReady() != prepared) { ExtensionVO updateExtension = extensionDao.createForUpdate(extension.getId()); - updateExtension.setEntryPointSync(sync); + updateExtension.setEntryPointReady(prepared); extensionDao.update(extension.getId(), updateExtension); } + return prepared; } @Override @@ -523,8 +570,12 @@ public Extension updateExtension(UpdateExtensionCmd cmd) { } return extensionVO; }); - if (StringUtils.isNotBlank(stateStr) && Extension.State.Enabled.equals(result.getState())) { - prepareExtensionEntryPointAcrossServers(result); + if (StringUtils.isNotBlank(stateStr) && Extension.State.Enabled.equals(result.getState()) && + !prepareExtensionEntryPointAcrossServers(result)) { + disableExtension(result.getId()); + throw new CloudRuntimeException(String.format( + "Failed to enable extension: %s as it entry-point is not ready", + extensionVO.getName())); } return result; } @@ -552,7 +603,7 @@ public boolean deleteExtension(DeleteExtensionCmd cmd) { return true; }); if (result && cleanup) { - cleanupExtensionEntryPointAcrossServers(extension); + cleanupExtensionFilesAcrossServers(extension); } return true; } @@ -639,7 +690,7 @@ public ExtensionResponse createExtensionResponse(Extension extension, extension.getDescription(), extension.getType().name()); response.setCreated(extension.getCreated()); response.setEntryPoint(externalProvisioner.getExtensionEntryPoint(extension.getRelativeEntryPoint())); - response.setEntryPointSync(extension.isEntryPointSync()); + response.setEntryPointReady(extension.isEntryPointReady()); response.setUserDefined(extension.isUserDefined()); response.setState(extension.getState().name()); if (viewDetails.contains(ApiConstants.ExtensionDetails.all) || @@ -685,7 +736,8 @@ public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { String description = cmd.getDescription(); Long extensionId = cmd.getExtensionId(); String resourceTypeStr = cmd.getResourceType(); - List rolesStrList = cmd.getRolesList(); + List rolesStrList = cmd.getAllowedRoleTypes(); + final int timeout = ObjectUtils.defaultIfNull(cmd.getTimeout(), 3); final boolean enabled = cmd.isEnabled(); Map parametersMap = cmd.getParametersMap(); final String successMessage = cmd.getSuccessMessage(); @@ -727,11 +779,11 @@ public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) { final ExtensionCustomAction.ResourceType resourceTypeFinal = resourceType; return Transaction.execute((TransactionCallbackWithException) status -> { ExtensionCustomActionVO customAction = - new ExtensionCustomActionVO(name, description, extensionId, successMessage, errorMessage, enabled); + new ExtensionCustomActionVO(name, description, extensionId, successMessage, errorMessage, timeout, enabled); if (resourceTypeFinal != null) { customAction.setResourceType(resourceTypeFinal); } - customAction.setRoles(RoleType.toCombinedMask(roleTypes)); + customAction.setAllowedRoleTypes(RoleType.toCombinedMask(roleTypes)); ExtensionCustomActionVO savedAction = extensionCustomActionDao.persist(customAction); List detailsVOList = new ArrayList<>(); detailsVOList.add(new ExtensionCustomActionDetailsVO( @@ -841,12 +893,13 @@ public ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd) { final long id = cmd.getId(); String description = cmd.getDescription(); String resourceTypeStr = cmd.getResourceType(); - List rolesStrList = cmd.getRoles(); + List rolesStrList = cmd.getAllowedRoleTypes(); Boolean enabled = cmd.isEnabled(); Map parametersMap = cmd.getParametersMap(); Boolean cleanupParameters = cmd.isCleanupParameters(); final String successMessage = cmd.getSuccessMessage(); final String errorMessage = cmd.getErrorMessage(); + final Integer timeout = cmd.getTimeout(); Map details = cmd.getDetails(); Boolean cleanupDetails = cmd.isCleanupDetails(); @@ -881,7 +934,7 @@ public ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd) { throw new InvalidParameterValueException(String.format("Invalid role specified - %s", roleTypeStr)); } } - customAction.setRoles(RoleType.toCombinedMask(roles)); + customAction.setAllowedRoleTypes(RoleType.toCombinedMask(roles)); needUpdate = true; } if (successMessage != null) { @@ -892,6 +945,10 @@ public ExtensionCustomAction updateCustomAction(UpdateCustomActionCmd cmd) { customAction.setErrorMessage(errorMessage); needUpdate = true; } + if (timeout != null) { + customAction.setTimeout(timeout); + needUpdate = true; + } if (enabled != null) { customAction.setEnabled(enabled); needUpdate = true; @@ -1055,19 +1112,22 @@ public CustomActionResultResponse runCustomAction(RunCustomActionCmd cmd) { response.setObjectName("customactionresult"); Map result = new HashMap<>(); response.setSuccess(false); - result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO)); + result.put(ApiConstants.MESSAGE, getActionMessage(false, customActionVO, extensionVO, + actionResourceType, entity)); Map externalDetails = getExternalAccessDetails(details, hostId, extensionResource, extensionVO); runCustomActionCommand.setParameters(parameters); runCustomActionCommand.setExternalDetails(externalDetails); try { Answer answer = agentMgr.send(hostId, runCustomActionCommand); if (!(answer instanceof RunCustomActionAnswer)) { - logger.error("Unexpected answer [{}] received for {}", answer.getClass().getSimpleName(), RunCustomActionCommand.class.getSimpleName()); + logger.error("Unexpected answer [{}] received for {}", answer.getClass().getSimpleName(), + RunCustomActionCommand.class.getSimpleName()); result.put(ApiConstants.DETAILS, error); } else { RunCustomActionAnswer customActionAnswer = (RunCustomActionAnswer) answer; response.setSuccess(answer.getResult()); - result.put(ApiConstants.MESSAGE, getActionMessage(answer.getResult(), customActionVO, extensionVO)); + result.put(ApiConstants.MESSAGE, getActionMessage(answer.getResult(), customActionVO, extensionVO, + actionResourceType, entity)); // ToDo: Check if we should pass the details for an errored action or pass it at all result.put(ApiConstants.DETAILS, customActionAnswer.getDetails()); } @@ -1091,10 +1151,14 @@ public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomA if (customAction.getResourceType() != null) { response.setResourceType(customAction.getResourceType().name()); } - Integer roles = ObjectUtils.defaultIfNull(customAction.getRoles(), RoleType.Admin.getMask()); - response.setRoles(RoleType.fromCombinedMask(roles).stream().map(Enum::name).collect(Collectors.toList())); + Integer roles = ObjectUtils.defaultIfNull(customAction.getAllowedRoleTypes(), RoleType.Admin.getMask()); + response.setAllowedRoleTypes(RoleType.fromCombinedMask(roles) + .stream() + .map(Enum::name) + .collect(Collectors.toList())); response.setSuccessMessage(customAction.getSuccessMessage()); response.setErrorMessage(customAction.getErrorMessage()); + response.setTimeout(customAction.getTimeout()); response.setEnabled(customAction.isEnabled()); response.setCreated(customAction.getCreated()); Optional.ofNullable(extensionDao.findById(customAction.getExtensionId())).ifPresent(extensionVO -> { @@ -1107,7 +1171,7 @@ public ExtensionCustomActionResponse createCustomActionResponse(ExtensionCustomA .ifPresent(parameters -> { Set paramResponses = parameters.stream() .map(p -> new ExtensionCustomActionParameterResponse(p.getName(), - p.getType().name(), p.getFormat().name(), p.getOptions(), p.isRequired())) + p.getType().name(), p.getValidationFormat().name(), p.getValueOptions(), p.isRequired())) .collect(Collectors.toSet()); response.setParameters(paramResponses); }); @@ -1136,7 +1200,8 @@ public Map getExternalAccessDetails(Host host) { return externalDetails; } - private Map getExternalAccessDetails(Map actionDetails, long hostId, ExtensionResourceMap resourceMap, Extension extension) { + private Map getExternalAccessDetails(Map actionDetails, long hostId, + ExtensionResourceMap resourceMap, Extension extension) { Map externalDetails = new HashMap<>(); externalDetails.put(ApiConstants.CUSTOM_ACTION_ID, actionDetails); Map hostDetails = getFilteredExternalDetails(hostDetailsDao.findDetails(hostId)); @@ -1172,9 +1237,9 @@ public String handleExtensionServerCommands(ExtensionServerActionBaseCommand com Pair result = prepareExtensionEntryPointOnCurrentServer( extensionName, cmd.isExtensionUserDefined(), extensionRelativeEntryPointPath); answer = new Answer(cmd, result.first(), result.second()); - } else if (command instanceof CleanupExtensionEntryPointCommand) { - final CleanupExtensionEntryPointCommand cmd = (CleanupExtensionEntryPointCommand)command; - Pair result = cleanupExtensionEntryPointOnCurrentServer(extensionName, + } else if (command instanceof CleanupExtensionFilesCommand) { + final CleanupExtensionFilesCommand cmd = (CleanupExtensionFilesCommand)command; + Pair result = cleanupExtensionFilesOnCurrentServer(extensionName, extensionRelativeEntryPointPath); answer = new Answer(cmd, result.first(), result.second()); } @@ -1224,46 +1289,30 @@ public List> getCommands() { } public class EntryPointSyncCheckWorker extends ManagedContextRunnable { - protected void sendExtensionEntryPointOutOfSyncAlert(Extension extension) { - String msg = String.format("Entry-point for %s are out of sync across management servers", - extension); - alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_USERVM, 0L, 0L, msg, msg); - } - protected void updateExtensionSync(Extension extension, boolean sync) { - if (!sync) { - sendExtensionEntryPointOutOfSyncAlert(extension); - } - if (extension.isEntryPointSync() == sync) { - return; - } - ExtensionVO extensionVO = extensionDao.createForUpdate(extension.getId()); - extensionVO.setEntryPointSync(sync); - extensionDao.update(extension.getId(), extensionVO); - } - - protected void checkSyncForOrchestrator(Extension extension, List msHosts) { + protected void checkExtensionEntryPointSync(Extension extension, List msHosts) { String checksum = externalProvisioner.getChecksumForExtensionEntryPoint(extension.getName(), extension.getRelativeEntryPoint()); if (StringUtils.isBlank(checksum)) { - updateExtensionSync(extension, false); + updateExtensionEntryPointReady(extension, false); return; } if (CollectionUtils.isEmpty(msHosts)) { - updateExtensionSync(extension, true); + updateExtensionEntryPointReady(extension, true); return; } for (ManagementServerHostVO msHost : msHosts) { - final Pair msPeerChecksumResult = getChecksumForExtensionEntryPointOnMSPeer(extension, msHost); + final Pair msPeerChecksumResult = getChecksumForExtensionEntryPointOnMSPeer(extension, + msHost); if (!msPeerChecksumResult.first() || !checksum.equals(msPeerChecksumResult.second())) { logger.error("Entry-point checksum for {} is different [msid: {}, checksum: {}] and [msid: {}, checksum: {}]", extension, ManagementServerNode.getManagementServerId(), checksum, msHost.getMsid(), (msPeerChecksumResult.first() ? msPeerChecksumResult.second() : "unknown")); - updateExtensionSync(extension, false); + updateExtensionEntryPointReady(extension, false); return; } } - updateExtensionSync(extension, true); + updateExtensionEntryPointReady(extension, true); } protected void runCleanupForLongestRunningManagementServer() { @@ -1275,12 +1324,9 @@ protected void runCleanupForLongestRunningManagementServer() { logger.debug("Skipping the extensions entrypoint sync check on this management server"); return; } - List extensions = extensionDao.listAll(); + List extensions = extensionDao.listAllEnabled(); for (ExtensionVO extension : extensions) { - if (!Extension.Type.Orchestrator.equals(extension.getType())) { - continue; - } - checkSyncForOrchestrator(extension, msHosts); + checkExtensionEntryPointSync(extension, msHosts); } } catch (Exception e) { logger.warn("Cleanup task failed to cleanup old webhook deliveries", e); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java index 1bc92bfb88c7..9426484e3851 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionCustomActionVO.java @@ -57,8 +57,8 @@ public class ExtensionCustomActionVO implements ExtensionCustomAction { @Enumerated(value = EnumType.STRING) private ResourceType resourceType; - @Column(name = "roles") - private Integer roles; + @Column(name = "allowed_role_types") + private Integer allowedRoleTypes; @Column(name = "success_message", length = 4096) private String successMessage; @@ -66,6 +66,9 @@ public class ExtensionCustomActionVO implements ExtensionCustomAction { @Column(name = "error_message", length = 4096) private String errorMessage; + @Column(name = "timeout", nullable = false) + private int timeout; + @Column(name = "enabled") private boolean enabled; @@ -82,7 +85,7 @@ public ExtensionCustomActionVO() { } public ExtensionCustomActionVO(String name, String description, long extensionId, String successMessage, - String errorMessage, boolean enabled) { + String errorMessage, int timeout, boolean enabled) { this.uuid = UUID.randomUUID().toString(); this.created = new Date(); this.name = name; @@ -90,6 +93,7 @@ public ExtensionCustomActionVO(String name, String description, long extensionId this.extensionId = extensionId; this.successMessage = successMessage; this.errorMessage = errorMessage; + this.timeout = timeout; this.enabled = enabled; } @@ -103,13 +107,13 @@ public String getUuid() { return uuid; } - public void setRoles(int roles) { - this.roles = roles; + public void setAllowedRoleTypes(int allowedRoleTypes) { + this.allowedRoleTypes = allowedRoleTypes; } @Override - public Integer getRoles() { - return roles; + public Integer getAllowedRoleTypes() { + return allowedRoleTypes; } public void setUuid(String uuid) { @@ -166,8 +170,13 @@ public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } - public void setCreated(Date created) { - this.created = created; + @Override + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; } @Override @@ -179,6 +188,10 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + public void setCreated(Date created) { + this.created = created; + } + @Override public Date getCreated() { return created; diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java index 4da99785d88e..4ff91b6b1190 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/vo/ExtensionVO.java @@ -52,7 +52,7 @@ public ExtensionVO(String name, String description, Type type, String relativeEn this.type = type; this.relativeEntryPoint = relativeEntryPoint; this.userDefined = true; - this.entryPointSync = true; + this.entryPointReady = true; this.state = state; this.created = new Date(); } @@ -78,8 +78,8 @@ public ExtensionVO(String name, String description, Type type, String relativeEn @Column(name = "relative_entry_point", nullable = false, length = 2048) private String relativeEntryPoint; - @Column(name = "entry_point_sync") - private boolean entryPointSync; + @Column(name = "entry_point_ready") + private boolean entryPointReady; @Column(name = "is_user_defined") private boolean userDefined; @@ -137,12 +137,12 @@ public void setRelativeEntryPoint(String relativeEntryPoint) { } @Override - public boolean isEntryPointSync() { - return entryPointSync; + public boolean isEntryPointReady() { + return entryPointReady; } - public void setEntryPointSync(boolean entryPointSync) { - this.entryPointSync = entryPointSync; + public void setEntryPointReady(boolean entryPointReady) { + this.entryPointReady = entryPointReady; } @Override diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java index c5820c04dd93..2f85d26bd8a1 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/agent/manager/ExternalAgentManagerImpl.java @@ -49,26 +49,22 @@ public boolean start() { @Override public List> getCommands() { - List> cmds = new ArrayList>(); - return cmds; + return new ArrayList<>(); } public Map> createServerResources(Map params) { - Map args = new HashMap<>(); Map> newResources = new HashMap<>(); ExternalResourceBase agentResource; synchronized (this) { String guid = (String)params.get("guid"); agentResource = new ExternalResourceBase(); - if (agentResource != null) { - try { - agentResource.start(); - agentResource.configure("ExternalHost-" + guid, params); - newResources.put(agentResource, args); - } catch (ConfigurationException e) { - logger.error("error while configuring server resource" + e.getMessage()); - } + try { + agentResource.start(); + agentResource.configure("ExternalHost-" + guid, params); + newResources.put(agentResource, args); + } catch (ConfigurationException e) { + logger.error("Error while configuring server resource {}", e.getMessage()); } } return newResources; diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java index 6c5694a5e09a..5c81e4678aad 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/discoverer/ExternalServerDiscoverer.java @@ -138,55 +138,54 @@ public boolean configure(String name, Map params) throws Configu @Override public Map> find(long dcId, Long podId, Long clusterId, URI uri, String username, String password, List hostTags) throws DiscoveryException { Map> resources; - - try { - String cluster = null; - if (clusterId == null) { - String msg = "must specify cluster Id when adding host"; - logger.debug(msg); - throw new RuntimeException(msg); - } else { - ClusterVO clu = _clusterDao.findById(clusterId); - if (clu == null || (clu.getHypervisorType() != Hypervisor.HypervisorType.External)) { - logger.info("invalid cluster id or cluster is not for Simulator hypervisors"); - return null; - } - cluster = Long.toString(clusterId); - if (clu.getGuid() == null) { - clu.setGuid(UUID.randomUUID().toString()); - } - _clusterDao.update(clusterId, clu); - } - - String pod; - if (podId == null) { - String msg = "must specify pod Id when adding host"; - logger.debug(msg); - throw new RuntimeException(msg); - } else { - pod = Long.toString(podId); - } - - Map params = new HashMap(); - params.put("username", username); - params.put("password", password); - params.put("zone", Long.toString(dcId)); - params.put("pod", pod); - params.put("cluster", cluster); - params.put("guid", uri.toString()); - - ExtensionResourceMapVO extensionResourceMapVO = extensionResourceMapDao.findByResourceIdAndType(clusterId, - ExtensionResourceMap.ResourceType.Cluster); - ExtensionVO extensionVO = extensionDao.findById(extensionResourceMapVO.getExtensionId()); - params.put("extensionName", extensionVO.getName()); - params.put("extensionRelativeEntryPoint", extensionVO.getRelativeEntryPoint()); - - resources = createAgentResources(params); - return resources; - } catch (Exception ex) { - logger.error("Exception when discovering external hosts: {}", ex.getMessage(), ex); + String errorMessage; + if (clusterId == null) { + errorMessage = "Must specify cluster Id when adding host"; + logger.error(errorMessage); + throw new DiscoveryException(errorMessage); } - return null; + ClusterVO cluster = _clusterDao.findById(clusterId); + if (cluster == null || (cluster.getHypervisorType() != Hypervisor.HypervisorType.External)) { + errorMessage = "Invalid cluster id or cluster is not for External hypervisors"; + logger.error(errorMessage); + throw new DiscoveryException(errorMessage); + } + if (podId == null) { + errorMessage = "Must specify pod when adding host"; + logger.error(errorMessage); + throw new DiscoveryException(errorMessage); + } + ExtensionResourceMapVO extensionResourceMapVO = extensionResourceMapDao.findByResourceIdAndType(clusterId, + ExtensionResourceMap.ResourceType.Cluster); + if (extensionResourceMapVO == null) { + logger.error("External hypervisor {} must be registered with an extension when adding host", + cluster); + throw new DiscoveryException("Cluster: %s is not registered with an extension"); + } + ExtensionVO extensionVO = extensionDao.findById(extensionResourceMapVO.getExtensionId()); + if (extensionVO == null) { + logger.error("Extension ID: {} to which {} cluster is registered is not found", + extensionResourceMapVO.getExtensionId(), cluster); + throw new DiscoveryException("Cluster: %s is registered with an inexistent extension"); + } + Map params = new HashMap<>(); + params.put("username", username); + params.put("password", password); + params.put("zone", Long.toString(dcId)); + params.put("pod", Long.toString(podId)); + params.put("cluster", Long.toString(clusterId)); + String url = uri.toString(); + params.put("hostname", url); + params.put("url", url); + String guid = UUID.nameUUIDFromBytes(url.getBytes()).toString(); + params.put("guid", guid); + params.put("extensionName", extensionVO.getName()); + params.put("extensionRelativeEntryPoint", extensionVO.getRelativeEntryPoint()); + resources = createAgentResources(params); + if (resources == null) { + throw new DiscoveryException("Failed to create external agent"); + } + return resources; } @Override diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java index 1f8023c389a8..5acac15cb4b1 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/provisioner/simpleprovisioner/SimpleExternalProvisioner.java @@ -28,10 +28,12 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import javax.inject.Inject; import javax.naming.ConfigurationException; @@ -110,12 +112,7 @@ public class SimpleExternalProvisioner extends ManagerBase implements ExternalPr @Override public String getName() { - return "simpleExternalProvisioner"; - } - - @Override - public String getDescription() { - return "Simple external provisioner"; + return getClass().getSimpleName(); } private String extensionsDirectory; @@ -168,6 +165,11 @@ protected boolean checkExtensionsDirectory() { return true; } + protected String getExtensionsPayloadBaseDirectory() { + // ToDo: get base payload directory from defined value in build.properties, etc + return extensionsDirectory + File.separator + "json"; + } + @Override public boolean configure(String name, Map params) throws ConfigurationException { super.configure(name, params); @@ -385,7 +387,6 @@ public RunCustomActionAnswer runCustomAction(String hostGuid, String extensionNa VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm); virtualMachineTO = hvGuru.implement(profile); } - logger.debug("Executing custom action '{}' in the external system", actionName); String prepareExternalScript = Script.findScript("", extensionPath); Map accessDetails = loadAccessDetails(externalDetails, virtualMachineTO); @@ -490,9 +491,48 @@ public void cleanupExtensionEntryPoint(String extensionName, String extensionRel } } + @Override + public void cleanupExtensionPayloads(String extensionName, int olderThanDays, boolean cleanupDirectory) { + String extensionPayloadDirPath = getExtensionsPayloadBaseDirectory() + File.separator + extensionName; + Path dirPath = Paths.get(extensionPayloadDirPath); + if (!Files.exists(dirPath)) { + return; + } + try { + if (cleanupDirectory) { + try (Stream paths = Files.walk(dirPath)) { + paths.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + return; + } + long cutoffMillis = System.currentTimeMillis() - (olderThanDays * 24L * 60 * 60 * 1000); + long lastModified = Files.getLastModifiedTime(dirPath).toMillis(); + if (lastModified < cutoffMillis) { + return; + } + try (Stream paths = Files.walk(dirPath)) { + paths.filter(path -> !path.equals(dirPath)) + .filter(path -> { + try { + return Files.getLastModifiedTime(path).toMillis() < cutoffMillis; + } catch (IOException e) { + return false; + } + }) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } catch (IOException e) { + logger.warn("Failed to clean up extension payloads for {}: {}", extensionName, e.getMessage()); + } + } + public Pair runCustomActionOnExternalSystem(String filename, String actionName, Map accessDetails, int wait) { - return executeExternalCommand(filename, actionName, accessDetails, wait, - String.format("Failed to execute custom action '%s' on external system", actionName)); + return executeExternalCommand(null, actionName, accessDetails, wait, + String.format("Failed to execute custom action '%s' on external system", actionName), filename); } private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map accessDetails, String extensionPath) { @@ -524,41 +564,42 @@ private VirtualMachine.PowerState getVmPowerState(UserVmVO userVmVO, Map prepareExternalProvisioningInternal(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "prepare", accessDetails, wait, - String.format("Failed to prepare external provisioner for deploying VM %s on external system", vmUUID)); + return executeExternalCommand(null, "prepare", accessDetails, wait, String.format("Failed to prepare external provisioner for deploying VM %s on external system", vmUUID), filename + ); } public Pair deployInstanceOnExternalSystem(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "create", accessDetails, wait, - String.format("Failed to create the instance %s on external system", vmUUID)); + return executeExternalCommand(null, "create", accessDetails, wait, String.format("Failed to create the instance %s on external system", vmUUID), filename + ); } public Pair startInstanceOnExternalSystem(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "start", accessDetails, wait, - String.format("Failed to start the instance %s on external system", vmUUID)); + return executeExternalCommand(null, "start", accessDetails, wait, String.format("Failed to start the instance %s on external system", vmUUID), filename + ); } public Pair stopInstanceOnExternalSystem(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "stop", accessDetails, wait, - String.format("Failed to stop the instance %s on external system", vmUUID)); + return executeExternalCommand(null, "stop", accessDetails, wait, String.format("Failed to stop the instance %s on external system", vmUUID), filename + ); } public Pair rebootInstanceOnExternalSystem(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "reboot", accessDetails, wait, - String.format("Failed to reboot the instance %s on external system", vmUUID)); + return executeExternalCommand(null, "reboot", accessDetails, wait, String.format("Failed to reboot the instance %s on external system", vmUUID), filename + ); } public Pair deleteInstanceOnExternalSystem(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "delete", accessDetails, wait, - String.format("Failed to delete the instance %s on external system", vmUUID)); + return executeExternalCommand(null, "delete", accessDetails, wait, String.format("Failed to delete the instance %s on external system", vmUUID), filename + ); } public Pair getInstanceStatusOnExternalSystem(String filename, String vmUUID, Map accessDetails, int wait) { - return executeExternalCommand(filename, "status", accessDetails, wait, - String.format("Failed to get the instance power status %s on external system", vmUUID)); + return executeExternalCommand(null, "status", accessDetails, wait, String.format("Failed to get the instance power status %s on external system", vmUUID), filename + ); } - public Pair executeExternalCommand(String file, String action, Map accessDetails, int wait, String errorLogPrefix) { + public Pair executeExternalCommand(String extensionName, String action, + Map accessDetails, int wait, String errorLogPrefix, String file) { try { Path executablePath = Paths.get(file).toAbsolutePath().normalize(); if (!Files.isExecutable(executablePath)) { @@ -568,7 +609,7 @@ public Pair executeExternalCommand(String file, String action, List command = new ArrayList<>(); command.add(executablePath.toString()); command.add(action); - String dataFile = prepareActionData(accessDetails); + String dataFile = prepareExternalPayload(extensionName, accessDetails); command.add(dataFile); command.add(Integer.toString(wait)); ProcessBuilder builder = new ProcessBuilder(command); @@ -618,16 +659,21 @@ public Answer checkHealth(String hostGuid, String extensionName, String extensio return new Answer(cmd); } - private String prepareActionData(Map details) throws IOException { - // ToDo: some mechanism to clean up these data files + private String prepareExternalPayload(String extensionName, Map details) throws IOException { String json = GsonHelper.getGson().toJson(details); logger.debug("Data: {}", json); long epochMillis = System.currentTimeMillis(); String fileName = epochMillis + ".json"; - Path tempDir = Files.createTempDirectory("orchestrator"); - Path tempFile = tempDir.resolve(fileName); - Files.writeString(tempFile, json, StandardOpenOption.CREATE_NEW); - return tempFile.toAbsolutePath().toString(); + String extensionPayloadDir = getExtensionsPayloadBaseDirectory() + File.separator + extensionName; + Path payloadDirPath = Paths.get(extensionPayloadDir); + if (!Files.exists(payloadDirPath)) { + Files.createDirectories(payloadDirPath); + } else { + cleanupExtensionPayloads(extensionName, 1, false); + } + Path payloadFile = payloadDirPath.resolve(fileName); + Files.writeString(payloadFile, json, StandardOpenOption.CREATE_NEW); + return payloadFile.toAbsolutePath().toString(); } @Override diff --git a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResourceBase.java b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResourceBase.java index 645770c318c6..5328b2cb7f58 100644 --- a/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResourceBase.java +++ b/plugins/hypervisors/external/src/main/java/org/apache/cloudstack/hypervisor/external/resource/ExternalResourceBase.java @@ -16,15 +16,11 @@ // under the License. package org.apache.cloudstack.hypervisor.external.resource; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.naming.ConfigurationException; -import org.apache.cloudstack.agent.manager.ExternalAgentManager; -import org.apache.cloudstack.agent.manager.ExternalAgentManagerImpl; import org.apache.cloudstack.hypervisor.external.provisioner.simpleprovisioner.SimpleExternalProvisioner; import com.cloud.agent.IAgentControl; @@ -64,26 +60,36 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.network.Networks; import com.cloud.resource.ServerResource; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ComponentContext; public class ExternalResourceBase implements ServerResource { + protected static final int CPU = 4; + protected static final long CPU_SPEED = 4000L; + protected static final long RAM = 16000 * 1024 * 1024L; + protected static final long DOM0_RAM = 768 * 1024 * 1024L; + protected static final String CAPABILITIES = "hvm"; - @Inject - ExternalAgentManager externalAgentManager = null; @Inject ExternalProvisioner externalProvisioner; + protected String url; protected String dcId; protected String pod; protected String cluster; + protected String name; protected String guid; - private Host.Type type; + private final Host.Type type; private String extensionName; private String extensionRelativeEntryPoint; + protected boolean isExtensionConnected() { + return StringUtils.isNoneBlank(extensionName, extensionRelativeEntryPoint); + } + public ExternalResourceBase() { - setType(Host.Type.Routing); + type = Host.Type.Routing; } @Override @@ -91,22 +97,16 @@ public Host.Type getType() { return type; } - public void setType(Host.Type type) { - this.type = type; - } - @Override public StartupCommand[] initialize() { - final List info = getHostInfo(); - final StartupRoutingCommand cmd = - new StartupRoutingCommand((Integer)info.get(0), (Long)info.get(1), (Long)info.get(2), (Long)info.get(4), (String)info.get(3), Hypervisor.HypervisorType.External, - Networks.RouterPrivateIpStrategy.HostLocal); + new StartupRoutingCommand(CPU, CPU_SPEED, RAM, DOM0_RAM, CAPABILITIES, + Hypervisor.HypervisorType.External,Networks.RouterPrivateIpStrategy.HostLocal); cmd.setDataCenter(dcId); cmd.setPod(pod); cmd.setCluster(cluster); - cmd.setHostType(Host.Type.Routing); - cmd.setName(guid); + cmd.setHostType(type); + cmd.setName(name); cmd.setPrivateIpAddress(Hypervisor.HypervisorType.External.toString()); cmd.setGuid(guid); cmd.setIqn(guid); @@ -114,24 +114,11 @@ public StartupCommand[] initialize() { return new StartupCommand[] {cmd}; } - protected List getHostInfo() { - final ArrayList info = new ArrayList<>(); - long speed = 4000L; - long cpus = 4L; - long ram = 16000L * 1024L * 1024L; - long dom0ram = Math.min(ram / 10, 768 * 1024 * 1024L); - - String cap = "hvm"; - info.add((int)cpus); - info.add(speed); - info.add(ram); - info.add(cap); - info.add(dom0ram); - return info; - } - @Override public PingCommand getCurrentStatus(long id) { + if (isExtensionConnected()) { + return null; + } final Map vmStates = externalProvisioner.getHostVmStateReport(id, extensionName, extensionRelativeEntryPoint); return new PingRoutingCommand(Host.Type.Routing, id, vmStates); @@ -282,17 +269,14 @@ public void setRunLevel(int level) { public boolean configure(String name, Map params) throws ConfigurationException { externalProvisioner = ComponentContext.inject(SimpleExternalProvisioner.class); externalProvisioner.configure(name, params); - externalAgentManager = ComponentContext.inject(ExternalAgentManagerImpl.class); - externalAgentManager.configure(name, params); - dcId = (String) params.get("zone"); pod = (String) params.get("pod"); cluster = (String) params.get("cluster"); + this.name = (String)params.get("name"); guid = (String) params.get("guid"); - url = (String) params.get("guid"); + url = (String) params.get("url"); extensionName = (String) params.get("extensionName"); extensionRelativeEntryPoint = (String) params.get("extensionRelativeEntryPoint"); - return true; } diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 809f87f0802d..3229560025a5 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -76,6 +76,7 @@ import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import com.cloud.agent.AgentManager; @@ -824,25 +825,7 @@ private List discoverHostsFull(final Long dcId, final Long podId, Long c } - try { - uri = new URI(UriUtils.encodeURIComponent(url)); - if (uri.getScheme() == null) { - throw new InvalidParameterValueException("uri.scheme is null " + url + ", add nfs:// (or cifs://) as a prefix"); - } else if (uri.getScheme().equalsIgnoreCase("nfs")) { - if (uri.getHost() == null || uri.getHost().equalsIgnoreCase("") || uri.getPath() == null || uri.getPath().equalsIgnoreCase("")) { - throw new InvalidParameterValueException("Your host and/or path is wrong. Make sure it's of the format nfs://hostname/path"); - } - } else if (uri.getScheme().equalsIgnoreCase("cifs")) { - // Don't validate against a URI encoded URI. - final URI cifsUri = new URI(url); - final String warnMsg = UriUtils.getCifsUriParametersProblems(cifsUri); - if (warnMsg != null) { - throw new InvalidParameterValueException(warnMsg); - } - } - } catch (final URISyntaxException e) { - throw new InvalidParameterValueException(url + " is not a valid uri"); - } + uri = validatedHostUrl(url, hypervisorType); final List hosts = new ArrayList<>(); logger.info("Trying to add a new host at {} in data center {}", url, zone); @@ -949,6 +932,33 @@ private List discoverHostsFull(final Long dcId, final Long podId, Long c throw new DiscoveryException("Unable to add the host: " + errorMsg); } + @NotNull + private static URI validatedHostUrl(String url, String hostHypervisorType) { + URI uri; + try { + uri = new URI(UriUtils.encodeURIComponent(url)); + if (uri.getScheme() == null) { + if (!HypervisorType.External.name().equalsIgnoreCase(hostHypervisorType)) { + throw new InvalidParameterValueException("uri.scheme is null " + url + ", add nfs:// (or cifs://) as a prefix"); + } + } else if (uri.getScheme().equalsIgnoreCase("nfs")) { + if (uri.getHost() == null || uri.getHost().equalsIgnoreCase("") || uri.getPath() == null || uri.getPath().equalsIgnoreCase("")) { + throw new InvalidParameterValueException("Your host and/or path is wrong. Make sure it's of the format nfs://hostname/path"); + } + } else if (uri.getScheme().equalsIgnoreCase("cifs")) { + // Don't validate against a URI encoded URI. + final URI cifsUri = new URI(url); + final String warnMsg = UriUtils.getCifsUriParametersProblems(cifsUri); + if (warnMsg != null) { + throw new InvalidParameterValueException(warnMsg); + } + } + } catch (final URISyntaxException e) { + throw new InvalidParameterValueException(url + " is not a valid uri"); + } + return uri; + } + @Override public Host getHost(final long hostId) { return _hostDao.findById(hostId); diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index c42690d212d0..dd6e487a0a87 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -375,6 +375,7 @@ "label.allocatedonly": "Allocated", "label.allocationstate": "Allocation state", "label.allow": "Allow", +"label.allowedroletypes": "Allowed Role Types", "label.allow.duplicate.macaddresses": "Allow duplicate MAC addresses", "label.allowuserdrivenbackups": "Allow User driven backups", "label.annotation": "Comment", @@ -562,6 +563,7 @@ "label.computeonly.offering.tooltip": "Option to specify root disk related information in the compute offering or to directly link a disk offering to the compute offering", "label.conditions": "Conditions", "label.configuration": "Configuration", +"label.configuration.details": "Configuration Details", "label.configure": "Configure", "label.configure.health.monitor": "Configure Health Monitor", "label.configure.app": "Configure the App", @@ -1003,6 +1005,7 @@ "label.export.rules": "Export Rules", "label.ext.hostname.tooltip": "External Host Name or IP Address", "label.extension": "Extension", +"label.extensions": "Extensions", "label.extensionid": "Extension", "label.extensionname": "Extension", "label.external": "External", @@ -1176,6 +1179,7 @@ "label.import.role": "Import role", "label.import.volume": "Import Volume", "label.inactive": "Inactive", +"label.inbuilt": "Inbuilt", "label.in.progress": "in progress", "label.in.progress.for": "in progress for", "label.info": "Info", @@ -1675,6 +1679,7 @@ "label.operator.equal": "Equals to", "label.optional": "Optional", "label.options": "Options", +"label.orchestrator": "Orchestrator", "label.order": "Order", "label.os": "Operating System", "label.oscategoryid": "OS category", @@ -2517,6 +2522,8 @@ "label.utilization": "Utilization", "label.uuid": "ID", "label.value": "Value", +"label.validationformat": "Validation Format", +"label.valueoptions": "Values Options", "label.vcenter": "VMware datacenter vCenter", "label.vcenter.datacenter": "vCenter datacenter", "label.vcenter.datastore": "vCenter datastore", @@ -2704,7 +2711,8 @@ "label.quotagib": "Quota in GiB", "label.encryption": "Encryption", "label.entrypoint": "Entry Point", -"label.entrypointsync": "Entry Point Sync", +"label.entrypointready": "Entry Point Ready", +"label.entrypointstate": "Entry Point State", "label.versioning": "Versioning", "label.objectlocking": "Object Lock", "label.bucket.policy": "Bucket Policy", @@ -2845,7 +2853,10 @@ "message.add.firewall.rule.processing": "Adding new Firewall rule...", "message.add.firewallrule.failed": "Adding Firewall Rule failed", "message.add.host": "Please specify the following parameters to add a new host.", -"message.add.external.details": "Details to be sent to external system on any operation.", +"message.add.extension.custom.action.details": "Details to be sent to the extension during execution of this action.", +"message.add.extension.details": "Details to be sent to the extension on any operation.", +"message.add.extension.resource.details": "Details to be sent to the extension during any operation for this resource.", +"message.add.orchestrator.resource.details": "Details to be sent to the hypervisor.", "message.add.host.sshkey": "WARNING: In order to add a host with SSH key, you must ensure your hypervisor host has been configured correctly.", "message.add.iprange.processing": "Adding IP Range...", "message.add.ipv4.subnet.for.guest.network.failed": "Failed to add IPv4 subnet for guest network", @@ -3105,6 +3116,8 @@ "message.desc.secondary.storage": "Each zone must have at least one NFS or secondary storage server. We will add the first one now. Secondary storage stores Instance Templates, ISO images, and Instance disk volume Snapshots. This server must be available to all hosts in the zone.

Provide the IP address and exported path.", "message.desc.register.user.data": "Please fill in the following data to register a User data.", "message.desc.registered.user.data": "Registered a User Data.", +"message.desc.validationformat": "Specifies the format used to validate the parameter value, such as EMAIL, URL, UUID, DECIMAL, etc.", +"message.desc.valueoptions": "Provide a comma-separated list of values that will appear as selectable options for this parameter", "message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.", "message.desc.zone.edge": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. An edge zone consists of one or more hosts (each of which provides local storage as primary storage servers). Only shared and L2 Networks can be deployed in such zones and functionalities that require secondary storages are not supported.", "message.drs.plan.description": "The maximum number of live migrations allowed for DRS. Configure DRS under the settings tab before generating a plan or to enable automatic DRS for the cluster.", @@ -3195,6 +3208,9 @@ "message.error.host.username": "Please enter host username.", "message.error.hypervisor.type": "Please select hypervisor type.", "message.error.input.value": "Please enter value.", +"message.error.input.invalidemail": "Please enter a valid email.", +"message.error.input.invalidurl": "Please enter a valid URL.", +"message.error.input.invaliduuid": "Please enter a valid UUID.", "message.error.internal.dns1": "Please enter internal DNS 1", "message.error.internallb.instance.port": "Please specify a Instance port.", "message.error.internallb.name": "Please specify a name for the internal LB.", diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 1865ced736fc..cfd4978841d9 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -127,7 +127,7 @@
{{ $t(getUserSourceLabel(dataResource[item])) }}
-
+
{{ dataResource[item].join(', ') }}
{{ dataResource[item] }}
@@ -177,7 +177,7 @@
- +
{{ $t('label.' + String(item).toLowerCase()) }}
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index c49c5b558ef4..ef276927aa18 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -114,6 +114,9 @@ }"/> + + {{ $t('label.inbuilt') }} +