mirror of https://github.com/apache/cloudstack.git
Merge 6cc3988f05 into 9bbd32a8ef
This commit is contained in:
commit
2355563ddc
|
|
@ -375,6 +375,7 @@ public class ApiConstants {
|
|||
public static final String LB_PROVIDER = "lbprovider";
|
||||
public static final String MAC_ADDRESS = "macaddress";
|
||||
public static final String MAC_ADDRESSES = "macaddresses";
|
||||
public static final String MANIFEST_URL = "manifesturl";
|
||||
public static final String MANUAL_UPGRADE = "manualupgrade";
|
||||
public static final String MATCH_TYPE = "matchtype";
|
||||
public static final String MAX = "max";
|
||||
|
|
|
|||
|
|
@ -50,5 +50,10 @@
|
|||
<version>4.23.0.0-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>${cs.jackson.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.api;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiCommandResourceType;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.ExtensionResponse;
|
||||
import org.apache.cloudstack.extension.Extension;
|
||||
import org.apache.cloudstack.framework.extensions.manager.ExtensionsImportManager;
|
||||
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
|
||||
|
||||
import com.cloud.exception.ConcurrentOperationException;
|
||||
import com.cloud.exception.NetworkRuleConflictException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.user.Account;
|
||||
|
||||
@APICommand(name = "importExtension",
|
||||
description = "Imports an extension",
|
||||
responseObject = ExtensionResponse.class,
|
||||
responseHasSensitiveInfo = false,
|
||||
entityType = {Extension.class},
|
||||
authorized = {RoleType.Admin},
|
||||
since = "4.23.0")
|
||||
public class ImportExtensionCmd extends BaseCmd {
|
||||
|
||||
@Inject
|
||||
ExtensionsManager extensionsManager;
|
||||
|
||||
@Inject
|
||||
ExtensionsImportManager extensionsImportManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Parameter(name = ApiConstants.MANIFEST_URL, type = CommandType.STRING, required = true,
|
||||
description = "URL of the extension manifest import file")
|
||||
private String manifestUrl;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public String getManifestUrl() {
|
||||
return manifestUrl;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void execute() throws ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
|
||||
Extension extension = extensionsImportManager.importExtension(this);
|
||||
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
|
||||
EnumSet.of(ApiConstants.ExtensionDetails.all));
|
||||
response.setResponseName(getCommandName());
|
||||
setResponseObject(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return Account.ACCOUNT_ID_SYSTEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiCommandResourceType getApiResourceType() {
|
||||
return ApiCommandResourceType.Extension;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.manager;
|
||||
|
||||
import org.apache.cloudstack.extension.Extension;
|
||||
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
|
||||
|
||||
public interface ExtensionsImportManager {
|
||||
Extension importExtension(ImportExtensionCmd cmd);
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.manager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.cloudstack.extension.Extension;
|
||||
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
|
||||
import org.apache.cloudstack.framework.extensions.dao.ExtensionDao;
|
||||
import org.apache.cloudstack.framework.extensions.util.ExtensionConfig;
|
||||
import org.apache.cloudstack.framework.extensions.util.YamlParser;
|
||||
import org.apache.cloudstack.framework.extensions.util.ZipExtractor;
|
||||
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.cloud.hypervisor.ExternalProvisioner;
|
||||
import com.cloud.utils.FileUtil;
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import com.cloud.utils.db.Transaction;
|
||||
import com.cloud.utils.db.TransactionCallbackWithException;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
public class ExtensionsImportManagerImpl extends ManagerBase implements ExtensionsImportManager {
|
||||
|
||||
@Inject
|
||||
ExtensionsManager extensionsManager;
|
||||
|
||||
@Inject
|
||||
ExternalProvisioner externalProvisioner;
|
||||
|
||||
@Inject
|
||||
ExtensionDao extensionDao;
|
||||
|
||||
protected Extension importExtensionInternal(String manifestUrl, Path tempDir) {
|
||||
Path manifestPath = tempDir.resolve("manifest.yaml");
|
||||
HttpUtils.downloadFileWithProgress(manifestUrl, manifestPath.toString(), logger);
|
||||
if (!Files.exists(manifestPath)) {
|
||||
throw new CloudRuntimeException("Failed to download extension manifest from URL: " + manifestUrl);
|
||||
}
|
||||
final ExtensionConfig extensionConfig = YamlParser.parseYamlFile(manifestPath.toString());
|
||||
//Parse the manifest and create the extension
|
||||
final String name = extensionConfig.metadata.name;
|
||||
final String extensionArchiveURL = extensionConfig.getArchiveUrl();
|
||||
ExtensionVO extensionByName = extensionDao.findByName(name);
|
||||
if (extensionByName != null) {
|
||||
throw new CloudRuntimeException("Extension by name already exists");
|
||||
}
|
||||
if (StringUtils.isBlank(extensionArchiveURL)) {
|
||||
throw new CloudRuntimeException("Unable to retrieve archive URL for extension source during import");
|
||||
}
|
||||
Path extensionArchivePath = tempDir.resolve(UUID.randomUUID() + ".zip");
|
||||
HttpUtils.downloadFileWithProgress(extensionArchiveURL, extensionArchivePath.toString(), logger);
|
||||
if (!Files.exists(extensionArchivePath)) {
|
||||
throw new CloudRuntimeException("Failed to download extension archive from URL: " + extensionArchiveURL);
|
||||
}
|
||||
final String extensionRootPath = externalProvisioner.getExtensionsPath() + File.separator + name;
|
||||
try {
|
||||
ZipExtractor.extractZipContents(extensionArchivePath.toString(), extensionRootPath);
|
||||
} catch (IOException e) {
|
||||
throw new CloudRuntimeException("Failed to extract extension archive during import at: " + extensionRootPath, e);
|
||||
}
|
||||
return Transaction.execute((TransactionCallbackWithException<Extension, CloudRuntimeException>) status -> {
|
||||
Extension extension = extensionsManager.createExtension(name, extensionConfig.metadata.description,
|
||||
extensionConfig.spec.type, extensionConfig.spec.entrypoint.path, Extension.State.Enabled.name(),
|
||||
false, Collections.emptyMap());
|
||||
|
||||
for (ExtensionConfig.CustomAction action : extensionConfig.spec.customActions) {
|
||||
Map<Integer, Map<String, String>> parameters = action.getParametersAsMap();
|
||||
extensionsManager.addCustomAction(action.name, action.description, extension.getId(),
|
||||
action.resourcetype, action.allowedroletypes, action.timeout, true, parameters,
|
||||
null, null, Collections.emptyMap());
|
||||
}
|
||||
return extension;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Extension importExtension(ImportExtensionCmd cmd) {
|
||||
final String manifestUrl = cmd.getManifestUrl();
|
||||
final String extensionsRootPath = externalProvisioner.getExtensionsPath();
|
||||
|
||||
Path tempDir;
|
||||
try {
|
||||
Path extensionsRootDir = Paths.get(extensionsRootPath);
|
||||
Files.createDirectories(extensionsRootDir);
|
||||
tempDir = Files.createTempDirectory(extensionsRootDir, "import-ext-");
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create working directory for import extension, {}", extensionsRootPath, e);
|
||||
throw new CloudRuntimeException("Failed to create working directory for import extension", e);
|
||||
}
|
||||
try {
|
||||
return importExtensionInternal(manifestUrl, tempDir);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
throw e;
|
||||
} finally {
|
||||
FileUtil.deletePath(tempDir.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ public interface ExtensionsManager extends Manager {
|
|||
|
||||
Extension createExtension(CreateExtensionCmd cmd);
|
||||
|
||||
Extension createExtension(String name, String description, String type, String relativePath, String state,
|
||||
Boolean orchestratorRequiresPrepareVm, Map<String, String> details);
|
||||
|
||||
boolean prepareExtensionPathAcrossServers(Extension extension);
|
||||
|
||||
List<ExtensionResponse> listExtensions(ListExtensionsCmd cmd);
|
||||
|
|
@ -79,6 +82,10 @@ public interface ExtensionsManager extends Manager {
|
|||
|
||||
ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd);
|
||||
|
||||
ExtensionCustomAction addCustomAction(String name, String description, long extensionId, String resourceTypeStr,
|
||||
List<String> rolesStrList, int timeout , boolean enabled, Map parametersMap, String successMessage,
|
||||
String errorMessage, Map<String, String> details);
|
||||
|
||||
boolean deleteCustomAction(DeleteCustomActionCmd cmd);
|
||||
|
||||
List<ExtensionCustomActionResponse> listCustomActions(ListCustomActionCmd cmd);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import org.apache.cloudstack.framework.extensions.api.AddCustomActionCmd;
|
|||
import org.apache.cloudstack.framework.extensions.api.CreateExtensionCmd;
|
||||
import org.apache.cloudstack.framework.extensions.api.DeleteCustomActionCmd;
|
||||
import org.apache.cloudstack.framework.extensions.api.DeleteExtensionCmd;
|
||||
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
|
||||
import org.apache.cloudstack.framework.extensions.api.ListCustomActionCmd;
|
||||
import org.apache.cloudstack.framework.extensions.api.ListExtensionsCmd;
|
||||
import org.apache.cloudstack.framework.extensions.api.RegisterExtensionCmd;
|
||||
|
|
@ -570,13 +571,8 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension")
|
||||
public Extension createExtension(CreateExtensionCmd cmd) {
|
||||
final String name = cmd.getName();
|
||||
final String description = cmd.getDescription();
|
||||
final String typeStr = cmd.getType();
|
||||
String relativePath = cmd.getPath();
|
||||
final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm();
|
||||
final String stateStr = cmd.getState();
|
||||
public Extension createExtension(String name, String description, String typeStr, String relativePath,
|
||||
String stateStr, Boolean orchestratorRequiresPrepareVm, Map<String, String> details) {
|
||||
ExtensionVO extensionByName = extensionDao.findByName(name);
|
||||
if (extensionByName != null) {
|
||||
throw new CloudRuntimeException("Extension by name already exists");
|
||||
|
|
@ -612,7 +608,6 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
}
|
||||
extension = extensionDao.persist(extension);
|
||||
|
||||
Map<String, String> details = cmd.getDetails();
|
||||
List<ExtensionDetailsVO> detailsVOList = new ArrayList<>();
|
||||
if (MapUtils.isNotEmpty(details)) {
|
||||
for (Map.Entry<String, String> entry : details.entrySet()) {
|
||||
|
|
@ -640,6 +635,22 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
return extensionVO;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CREATE, eventDescription = "creating extension")
|
||||
public Extension createExtension(CreateExtensionCmd cmd) {
|
||||
final String name = cmd.getName();
|
||||
final String description = cmd.getDescription();
|
||||
final String typeStr = cmd.getType();
|
||||
String relativePath = cmd.getPath();
|
||||
final Boolean orchestratorRequiresPrepareVm = cmd.isOrchestratorRequiresPrepareVm();
|
||||
final String stateStr = cmd.getState();
|
||||
final Map<String, String> details = cmd.getDetails();
|
||||
return createExtension(name, description, typeStr, relativePath, stateStr, orchestratorRequiresPrepareVm,
|
||||
details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepareExtensionPathAcrossServers(Extension extension) {
|
||||
boolean prepared = true;
|
||||
|
|
@ -973,18 +984,10 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action")
|
||||
public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) {
|
||||
String name = cmd.getName();
|
||||
String description = cmd.getDescription();
|
||||
Long extensionId = cmd.getExtensionId();
|
||||
String resourceTypeStr = cmd.getResourceType();
|
||||
List<String> 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();
|
||||
final String errorMessage = cmd.getErrorMessage();
|
||||
Map<String, String> details = cmd.getDetails();
|
||||
public ExtensionCustomAction addCustomAction(String name, String description, long extensionId,
|
||||
String resourceTypeStr, List<String> rolesStrList, int timeout , boolean enabled, Map parametersMap,
|
||||
String successMessage, String errorMessage, Map<String, String> details) {
|
||||
|
||||
if (name == null || !name.matches("^[a-zA-Z0-9 _-]+$")) {
|
||||
throw new InvalidParameterValueException(String.format("Invalid action name: %s. It can contain " +
|
||||
"only alphabets, numbers, hyphen, underscore and space", name));
|
||||
|
|
@ -1004,7 +1007,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
if (resourceType == null) {
|
||||
throw new InvalidParameterValueException(
|
||||
String.format("Invalid resource type specified: %s. Valid values are: %s", resourceTypeStr,
|
||||
EnumSet.allOf(ExtensionCustomAction.ResourceType.class)));
|
||||
EnumSet.allOf(ExtensionCustomAction.ResourceType.class)));
|
||||
}
|
||||
}
|
||||
if (resourceType == null && Extension.Type.Orchestrator.equals(extensionVO.getType())) {
|
||||
|
|
@ -1050,6 +1053,24 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_ADD, eventDescription = "adding extension custom action")
|
||||
public ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd) {
|
||||
String name = cmd.getName();
|
||||
String description = cmd.getDescription();
|
||||
Long extensionId = cmd.getExtensionId();
|
||||
String resourceTypeStr = cmd.getResourceType();
|
||||
List<String> 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();
|
||||
final String errorMessage = cmd.getErrorMessage();
|
||||
Map<String, String> details = cmd.getDetails();
|
||||
return addCustomAction(name, description, extensionId, resourceTypeStr, rolesStrList, timeout, enabled,
|
||||
parametersMap, successMessage, errorMessage, details);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_EXTENSION_CUSTOM_ACTION_DELETE, eventDescription = "deleting extension custom action")
|
||||
public boolean deleteCustomAction(DeleteCustomActionCmd cmd) {
|
||||
|
|
@ -1637,6 +1658,7 @@ public class ExtensionsManagerImpl extends ManagerBase implements ExtensionsMana
|
|||
cmds.add(RunCustomActionCmd.class);
|
||||
|
||||
cmds.add(CreateExtensionCmd.class);
|
||||
cmds.add(ImportExtensionCmd.class);
|
||||
cmds.add(ListExtensionsCmd.class);
|
||||
cmds.add(DeleteExtensionCmd.class);
|
||||
cmds.add(UpdateExtensionCmd.class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ExtensionConfig {
|
||||
|
||||
public String apiVersion;
|
||||
public String kind;
|
||||
public Metadata metadata;
|
||||
public Source source;
|
||||
public Spec spec;
|
||||
|
||||
String archiveUrl;
|
||||
|
||||
// -----------------------------
|
||||
// Nested compact model
|
||||
// -----------------------------
|
||||
|
||||
public static class Metadata {
|
||||
public String name;
|
||||
public String displayName;
|
||||
public String description;
|
||||
public String version;
|
||||
public String maintainer;
|
||||
public String homepage;
|
||||
}
|
||||
|
||||
public static class Source {
|
||||
public String type;
|
||||
public String url;
|
||||
public String refs;
|
||||
}
|
||||
|
||||
public static class Spec {
|
||||
public String type;
|
||||
public Compatibility compatibility;
|
||||
public Entrypoint entrypoint;
|
||||
public Orchestrator orchestrator;
|
||||
private Map<String, String> details;
|
||||
public boolean enabled;
|
||||
public List<CustomAction> customActions;
|
||||
|
||||
public Map<String, String> getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(Map<String, String> details) {
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Compatibility {
|
||||
public CloudStack cloudstack;
|
||||
}
|
||||
|
||||
public static class CloudStack {
|
||||
public String minVersion;
|
||||
}
|
||||
|
||||
public static class Entrypoint {
|
||||
public String language;
|
||||
public String path;
|
||||
public String targetDir;
|
||||
}
|
||||
|
||||
public static class Orchestrator {
|
||||
public boolean requiresPrepareVm;
|
||||
}
|
||||
|
||||
public static class CustomAction {
|
||||
public String name;
|
||||
public String displayName;
|
||||
public String description;
|
||||
public String resourcetype;
|
||||
public boolean enabled;
|
||||
public int timeout;
|
||||
public List<String> allowedroletypes;
|
||||
public List<Parameter> parameters;
|
||||
|
||||
public Map<Integer, Map<String, String>> getParametersAsMap() {
|
||||
Map<Integer, Map<String, String>> paramMap = new HashMap<>();
|
||||
int index = 0;
|
||||
for (Parameter param : parameters) {
|
||||
Map<String, String> singleParamMap = new HashMap<>();
|
||||
singleParamMap.put(ApiConstants.NAME, param.name);
|
||||
singleParamMap.put(ApiConstants.TYPE, param.type);
|
||||
singleParamMap.put(ApiConstants.VALIDATION_FORMAT, param.validationformat);
|
||||
singleParamMap.put(ApiConstants.REQUIRED, Boolean.toString(param.required));
|
||||
paramMap.put(index++, singleParamMap);
|
||||
}
|
||||
return paramMap;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Parameter {
|
||||
public String name;
|
||||
public String type;
|
||||
public String validationformat;
|
||||
public boolean required;
|
||||
}
|
||||
|
||||
public String getArchiveUrl() {
|
||||
String type = source != null ? source.type : null;
|
||||
if ("git".equalsIgnoreCase(type) && source.url != null && source.url.contains("github.com")) {
|
||||
// ToDo: improve
|
||||
String ref = source.refs != null ? source.refs : "main";
|
||||
return source.url.replace("github.com", "codeload.github.com") + "/zip/refs/heads/" + ref;
|
||||
}
|
||||
return source == null ? null : source.url;
|
||||
}
|
||||
|
||||
public Spec getSpec() {
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
|
||||
public class YamlParser {
|
||||
private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
|
||||
|
||||
public static ExtensionConfig parseYamlFile(String filePath) {
|
||||
ExtensionConfig extensionConfig = null;
|
||||
try (InputStream in = Files.newInputStream(Path.of(filePath))) {
|
||||
extensionConfig = mapper.readValue(in, ExtensionConfig.class);
|
||||
} catch (Exception ex) {
|
||||
throw new CloudRuntimeException("Failed to parse YAML", ex);
|
||||
}
|
||||
return extensionConfig;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class ZipExtractor {
|
||||
|
||||
/**
|
||||
* Extracts a GitHub ZIP file contents directly into destDir, skipping top-level folder.
|
||||
*
|
||||
* @param zipFilePath Path to the ZIP file
|
||||
* @param destDir Destination directory
|
||||
*/
|
||||
public static void extractZipContents(String zipFilePath, String destDir) throws IOException {
|
||||
Path destPath = Paths.get(destDir);
|
||||
if (!Files.exists(destPath)) {
|
||||
Files.createDirectories(destPath);
|
||||
}
|
||||
|
||||
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(Paths.get(zipFilePath)))) {
|
||||
ZipEntry entry;
|
||||
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
String entryName = entry.getName();
|
||||
|
||||
// Skip the top-level folder (everything before first '/')
|
||||
int firstSlash = entryName.indexOf('/');
|
||||
if (firstSlash >= 0) {
|
||||
entryName = entryName.substring(firstSlash + 1);
|
||||
}
|
||||
|
||||
if (entryName.isEmpty()) {
|
||||
zis.closeEntry();
|
||||
continue; // skip the top-level folder itself
|
||||
}
|
||||
|
||||
Path newPath = safeResolve(destPath, entryName);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(newPath);
|
||||
} else {
|
||||
if (newPath.getParent() != null) {
|
||||
Files.createDirectories(newPath.getParent());
|
||||
}
|
||||
try (OutputStream os = Files.newOutputStream(newPath)) {
|
||||
zis.transferTo(os);
|
||||
}
|
||||
}
|
||||
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Protects from ZIP Slip vulnerability.
|
||||
*/
|
||||
private static Path safeResolve(Path destDir, String entryName) throws IOException {
|
||||
Path resolved = destDir.resolve(entryName).normalize();
|
||||
if (!resolved.startsWith(destDir)) {
|
||||
throw new IOException("ZIP entry outside target dir: " + entryName);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
<bean id="ExtensionResourceMapDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDaoImpl" />
|
||||
<bean id="ExtensionResourceMapDetailsDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionResourceMapDetailsDaoImpl" />
|
||||
<bean id="ExtensionsManager" class="org.apache.cloudstack.framework.extensions.manager.ExtensionsManagerImpl" />
|
||||
<bean id="ExtensionsImportManager" class="org.apache.cloudstack.framework.extensions.manager.ExtensionsImportManagerImpl" />
|
||||
<bean id="ExtensionCustomActionDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDaoImpl" />
|
||||
<bean id="ExtensionCustomActionDetailsDaoImpl" class="org.apache.cloudstack.framework.extensions.dao.ExtensionCustomActionDetailsDaoImpl" />
|
||||
</beans>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.framework.extensions.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class YamlParserTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testParseYaml() {
|
||||
String yamlFilePath = getClass().getResource("testmanifest.yaml").getFile();
|
||||
ExtensionConfig config = YamlParser.parseYamlFile(yamlFilePath);
|
||||
assertNotNull(config);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
apiVersion: cloudstack.apache.org/v1
|
||||
kind: OrchestratorExtension
|
||||
|
||||
metadata:
|
||||
name: test
|
||||
displayName: Test Extension
|
||||
description: >
|
||||
Test extension via the Orchestrator Extension Framework.
|
||||
version: 0.1.0
|
||||
maintainer: "Test Maintainer <maintainer@test.com>"
|
||||
homepage: "https://github.com/maintainer/test"
|
||||
|
||||
source:
|
||||
type: git
|
||||
url: "https://github.com/maintainer/test"
|
||||
refs: "branchName"
|
||||
|
||||
spec:
|
||||
type: Orchestrator
|
||||
|
||||
compatibility:
|
||||
cloudstack:
|
||||
minVersion: 4.23.0
|
||||
|
||||
entrypoint:
|
||||
path: test.py
|
||||
targetDir: /usr/share/cloudstack-management/extensions/test
|
||||
|
||||
orchestrator:
|
||||
requiresPrepareVm: false
|
||||
|
||||
details:
|
||||
key1: value1
|
||||
key2: value2
|
||||
|
||||
enabled: true
|
||||
|
||||
customActions:
|
||||
- name: CreateSnapshot
|
||||
displayName: "Create Snapshot"
|
||||
description: "Create a snapshot for a virtual machine instance."
|
||||
resourcetype: VirtualMachine
|
||||
enabled: true
|
||||
timeout: 600
|
||||
allowedroletypes: [Admin, DomainAdmin, User]
|
||||
parameters:
|
||||
- name: snap_name
|
||||
type: STRING
|
||||
validationformat: NONE
|
||||
required: true
|
||||
- name: snap_description
|
||||
type: STRING
|
||||
validationformat: NONE
|
||||
required: false
|
||||
- name: snap_save_memory
|
||||
type: BOOLEAN
|
||||
validationformat: NONE
|
||||
required: false
|
||||
|
||||
- name: ListSnapshots
|
||||
displayName: "List Snapshots"
|
||||
description: "List all snapshots for a virtual machine instance."
|
||||
resourcetype: VirtualMachine
|
||||
enabled: true
|
||||
timeout: 120
|
||||
allowedroletypes: [Admin, DomainAdmin, User]
|
||||
parameters: []
|
||||
|
||||
- name: RestoreSnapshot
|
||||
displayName: "Restore Snapshot"
|
||||
description: "Restore a virtual machine instance from a given snapshot."
|
||||
resourcetype: VirtualMachine
|
||||
enabled: true
|
||||
timeout: 900
|
||||
allowedroletypes: [Admin, DomainAdmin, User]
|
||||
parameters:
|
||||
- name: snap_name
|
||||
type: STRING
|
||||
validationformat: NONE
|
||||
required: true
|
||||
|
||||
- name: DeleteSnapshot
|
||||
displayName: "Delete Snapshot"
|
||||
description: "Delete a snapshot for a virtual machine instance."
|
||||
resourcetype: VirtualMachine
|
||||
enabled: true
|
||||
timeout: 300
|
||||
allowedroletypes: [Admin, DomainAdmin, User]
|
||||
parameters:
|
||||
- name: snap_name
|
||||
type: STRING
|
||||
validationformat: NONE
|
||||
required: true
|
||||
|
|
@ -1274,6 +1274,7 @@
|
|||
"label.images": "Images",
|
||||
"label.imagestoreid": "Secondary Storage",
|
||||
"label.import.backup.offering": "Import Backup Offering",
|
||||
"label.import.extension": "Import Extension",
|
||||
"label.import.instance": "Import Instance",
|
||||
"label.import.offering": "Import Offering",
|
||||
"label.import.role": "Import Role",
|
||||
|
|
@ -1548,6 +1549,7 @@
|
|||
"label.management.servers": "Management Servers",
|
||||
"label.management.server.peers": "Peers",
|
||||
"label.managementservers": "Number of management servers",
|
||||
"label.manifesturl": "Manifest URL",
|
||||
"label.matchall": "Match all",
|
||||
"label.matchtype": "Match Type",
|
||||
"label.max": "Max.",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,15 @@ export default {
|
|||
popup: true,
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/CreateExtension.vue')))
|
||||
},
|
||||
{
|
||||
api: 'importExtension',
|
||||
icon: 'cloud-upload-outlined',
|
||||
label: 'label.import.extension',
|
||||
docHelp: 'adminguide/extensions.html',
|
||||
listView: true,
|
||||
popup: true,
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/extension/ImportExtension.vue')))
|
||||
},
|
||||
{
|
||||
api: 'updateExtension',
|
||||
icon: 'edit-outlined',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<div class="form-layout" v-ctrl-enter="handleSubmit">
|
||||
<a-form
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:loading="loading"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit">
|
||||
<a-form-item name="name" ref="name">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.manifesturl')" :tooltip="apiParams.manifesturl.description"/>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:value="form.manifesturl"
|
||||
:placeholder="apiParams.manifesturl.description"
|
||||
v-focus="true" />
|
||||
</a-form-item>
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { postAPI } from '@/api'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
|
||||
export default {
|
||||
name: 'ImportExtension',
|
||||
components: {
|
||||
TooltipLabel
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.apiParams = this.$getApiParams('importExtension')
|
||||
},
|
||||
created () {
|
||||
this.initForm()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
this.rules = reactive({
|
||||
manifesturl: [{ required: true, message: `${this.$t('message.error.input.value')}` }]
|
||||
})
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
const values = toRaw(this.form)
|
||||
this.loading = true
|
||||
const params = {
|
||||
manifesturl: values.manifesturl
|
||||
}
|
||||
postAPI('importExtension', params).then(response => {
|
||||
this.$emit('refresh-data')
|
||||
this.$notification.success({
|
||||
message: this.$t('label.create.extension'),
|
||||
description: this.$t('message.success.create.extension')
|
||||
})
|
||||
this.closeAction()
|
||||
}).catch(error => {
|
||||
this.$notification.error({
|
||||
message: this.$t('message.request.failed'),
|
||||
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
|
||||
duration: 0
|
||||
})
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
})
|
||||
},
|
||||
closeAction () {
|
||||
this.$emit('close-action')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-layout {
|
||||
width: 80vw;
|
||||
@media (min-width: 600px) {
|
||||
width: 550px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue