Skip to content

Commit 400f292

Browse files
committed
feature: import extension using yaml manifest
Adds import functionality for extension by giving an YAML manifest URL. Manifest will define extension and its custom actions details. Co-authored-by: Manoj Kumar <manojkr.itbhu@gmail.com> Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
1 parent 6dc259c commit 400f292

File tree

17 files changed

+817
-21
lines changed

17 files changed

+817
-21
lines changed

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ public class ApiConstants {
374374
public static final String LB_PROVIDER = "lbprovider";
375375
public static final String MAC_ADDRESS = "macaddress";
376376
public static final String MAC_ADDRESSES = "macaddresses";
377+
public static final String MANIFEST_URL = "manifesturl";
377378
public static final String MANUAL_UPGRADE = "manualupgrade";
378379
public static final String MAX = "max";
379380
public static final String MAX_SNAPS = "maxsnaps";

framework/extensions/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,10 @@
5050
<version>4.23.0.0-SNAPSHOT</version>
5151
<scope>compile</scope>
5252
</dependency>
53+
<dependency>
54+
<groupId>com.fasterxml.jackson.dataformat</groupId>
55+
<artifactId>jackson-dataformat-yaml</artifactId>
56+
<version>${cs.jackson.version}</version>
57+
</dependency>
5358
</dependencies>
5459
</project>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.framework.extensions.api;
19+
20+
import java.util.EnumSet;
21+
22+
import javax.inject.Inject;
23+
24+
import org.apache.cloudstack.acl.RoleType;
25+
import org.apache.cloudstack.api.APICommand;
26+
import org.apache.cloudstack.api.ApiCommandResourceType;
27+
import org.apache.cloudstack.api.ApiConstants;
28+
import org.apache.cloudstack.api.BaseCmd;
29+
import org.apache.cloudstack.api.Parameter;
30+
import org.apache.cloudstack.api.ServerApiException;
31+
import org.apache.cloudstack.api.response.ExtensionResponse;
32+
import org.apache.cloudstack.extension.Extension;
33+
import org.apache.cloudstack.framework.extensions.manager.ExtensionsImportManager;
34+
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
35+
36+
import com.cloud.exception.ConcurrentOperationException;
37+
import com.cloud.exception.NetworkRuleConflictException;
38+
import com.cloud.exception.ResourceAllocationException;
39+
import com.cloud.user.Account;
40+
41+
@APICommand(name = "importExtension",
42+
description = "Imports an extension",
43+
responseObject = ExtensionResponse.class,
44+
responseHasSensitiveInfo = false,
45+
entityType = {Extension.class},
46+
authorized = {RoleType.Admin},
47+
since = "4.23.0")
48+
public class ImportExtensionCmd extends BaseCmd {
49+
50+
@Inject
51+
ExtensionsManager extensionsManager;
52+
53+
@Inject
54+
ExtensionsImportManager extensionsImportManager;
55+
56+
/////////////////////////////////////////////////////
57+
//////////////// API parameters /////////////////////
58+
/////////////////////////////////////////////////////
59+
60+
@Parameter(name = ApiConstants.MANIFEST_URL, type = CommandType.STRING, required = true,
61+
description = "URL of the extension manifest import file")
62+
private String manifestUrl;
63+
64+
/////////////////////////////////////////////////////
65+
/////////////////// Accessors ///////////////////////
66+
/////////////////////////////////////////////////////
67+
68+
public String getManifestUrl() {
69+
return manifestUrl;
70+
}
71+
72+
/////////////////////////////////////////////////////
73+
/////////////// API Implementation///////////////////
74+
/////////////////////////////////////////////////////
75+
76+
@Override
77+
public void execute() throws ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
78+
Extension extension = extensionsImportManager.importExtension(this);
79+
ExtensionResponse response = extensionsManager.createExtensionResponse(extension,
80+
EnumSet.of(ApiConstants.ExtensionDetails.all));
81+
response.setResponseName(getCommandName());
82+
setResponseObject(response);
83+
}
84+
85+
@Override
86+
public long getEntityOwnerId() {
87+
return Account.ACCOUNT_ID_SYSTEM;
88+
}
89+
90+
@Override
91+
public ApiCommandResourceType getApiResourceType() {
92+
return ApiCommandResourceType.Extension;
93+
}
94+
95+
96+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.framework.extensions.manager;
19+
20+
import org.apache.cloudstack.extension.Extension;
21+
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
22+
23+
public interface ExtensionsImportManager {
24+
Extension importExtension(ImportExtensionCmd cmd);
25+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.cloudstack.framework.extensions.manager;
19+
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.util.Collection;
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.UUID;
31+
32+
import javax.inject.Inject;
33+
34+
import org.apache.cloudstack.extension.Extension;
35+
import org.apache.cloudstack.framework.extensions.api.ImportExtensionCmd;
36+
import org.apache.cloudstack.framework.extensions.dao.ExtensionDao;
37+
import org.apache.cloudstack.framework.extensions.util.ExtensionConfig;
38+
import org.apache.cloudstack.framework.extensions.util.YamlParser;
39+
import org.apache.cloudstack.framework.extensions.util.ZipExtractor;
40+
import org.apache.cloudstack.framework.extensions.vo.ExtensionVO;
41+
import org.apache.commons.lang3.StringUtils;
42+
43+
import com.cloud.hypervisor.ExternalProvisioner;
44+
import com.cloud.utils.HttpUtils;
45+
import com.cloud.utils.component.ManagerBase;
46+
import com.cloud.utils.db.Transaction;
47+
import com.cloud.utils.db.TransactionCallbackWithException;
48+
import com.cloud.utils.exception.CloudRuntimeException;
49+
50+
public class ExtensionsImportManagerImpl extends ManagerBase implements ExtensionsImportManager {
51+
52+
@Inject
53+
ExtensionsManager extensionsManager;
54+
55+
@Inject
56+
ExternalProvisioner externalProvisioner;
57+
58+
@Inject
59+
ExtensionDao extensionDao;
60+
61+
protected Extension importExtensionInternal(String manifestUrl, Path tempDir) {
62+
Path manifestPath = tempDir.resolve("manifest.yaml");
63+
HttpUtils.downloadFileWithProgress(manifestUrl, manifestPath.toString(), logger);
64+
if (!Files.exists(manifestPath)) {
65+
throw new CloudRuntimeException("Failed to download extension manifest from URL: " + manifestUrl);
66+
}
67+
final ExtensionConfig extensionConfig = YamlParser.parseYamlFile(manifestPath.toString());
68+
//Parse the manifest and create the extension
69+
final String name = extensionConfig.metadata.name;
70+
final String extensionArchiveURL = extensionConfig.getArchiveUrl();
71+
ExtensionVO extensionByName = extensionDao.findByName(name);
72+
if (extensionByName != null) {
73+
throw new CloudRuntimeException("Extension by name already exists");
74+
}
75+
if (StringUtils.isBlank(extensionArchiveURL)) {
76+
throw new CloudRuntimeException("Unable to retrieve archive URL for extension source during import");
77+
}
78+
Path extensionArchivePath = tempDir.resolve(UUID.randomUUID() + ".zip");
79+
HttpUtils.downloadFileWithProgress(extensionArchiveURL, extensionArchivePath.toString(), logger);
80+
if (!Files.exists(extensionArchivePath)) {
81+
throw new CloudRuntimeException("Failed to download extension archive from URL: " + extensionArchiveURL);
82+
}
83+
final String extensionRootPath = externalProvisioner.getExtensionsPath() + File.separator + name;
84+
try {
85+
ZipExtractor.extractZipContents(extensionArchivePath.toString(), extensionRootPath);
86+
} catch (IOException e) {
87+
throw new CloudRuntimeException("Failed to extract extension archive during import at: " + extensionRootPath, e);
88+
}
89+
return Transaction.execute((TransactionCallbackWithException<Extension, CloudRuntimeException>) status -> {
90+
Extension extension = extensionsManager.createExtension(name, extensionConfig.metadata.description,
91+
extensionConfig.spec.type, extensionConfig.spec.entrypoint.path, Extension.State.Enabled.name(),
92+
false, Collections.emptyMap());
93+
94+
for (ExtensionConfig.CustomAction action : extensionConfig.spec.customActions) {
95+
List<Map<String, String>> parameters = action.getParametersMapList();
96+
Map<Integer, Collection<Map<String, String>>> parametersMap = new HashMap<>();
97+
parametersMap.put(1, parameters);
98+
extensionsManager.addCustomAction(action.name, action.description, extension.getId(),
99+
action.resourcetype, action.allowedroletypes, action.timeout, true, parametersMap,
100+
null, null, Collections.emptyMap());
101+
}
102+
return null;
103+
});
104+
}
105+
106+
@Override
107+
public Extension importExtension(ImportExtensionCmd cmd) {
108+
final String manifestUrl = cmd.getManifestUrl();
109+
final String extensionsRootPath = externalProvisioner.getExtensionsPath();
110+
111+
Path tempDir;
112+
try {
113+
Path extensionsRootDir = Paths.get(extensionsRootPath);
114+
Files.createDirectories(extensionsRootDir);
115+
tempDir = Files.createTempDirectory(extensionsRootDir, "import-ext-");
116+
117+
} catch (IOException e) {
118+
logger.error("Failed to create working directory for import extension, {}", extensionsRootPath, e);
119+
throw new CloudRuntimeException("Failed to create working directory for import extension", e);
120+
}
121+
try {
122+
return importExtensionInternal(manifestUrl, tempDir);
123+
} catch (Exception e) {
124+
logger.error(e.getMessage(), e);
125+
throw e;
126+
}/* finally {
127+
FileUtil.deletePath(tempDir.toString());
128+
}*/
129+
}
130+
}

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManager.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ public interface ExtensionsManager extends Manager {
5757

5858
Extension createExtension(CreateExtensionCmd cmd);
5959

60+
Extension createExtension(String name, String description, String type, String relativePath, String state,
61+
Boolean orchestratorRequiresPrepareVm, Map<String, String> details);
62+
6063
boolean prepareExtensionPathAcrossServers(Extension extension);
6164

6265
List<ExtensionResponse> listExtensions(ListExtensionsCmd cmd);
@@ -79,6 +82,10 @@ public interface ExtensionsManager extends Manager {
7982

8083
ExtensionCustomAction addCustomAction(AddCustomActionCmd cmd);
8184

85+
ExtensionCustomAction addCustomAction(String name, String description, long extensionId, String resourceTypeStr,
86+
List<String> rolesStrList, int timeout , boolean enabled, Map parametersMap, String successMessage,
87+
String errorMessage, Map<String, String> details);
88+
8289
boolean deleteCustomAction(DeleteCustomActionCmd cmd);
8390

8491
List<ExtensionCustomActionResponse> listCustomActions(ListCustomActionCmd cmd);

0 commit comments

Comments
 (0)