Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2026-01-22 10:18:07 +05:30
parent 7c23b2610a
commit 065ec85589
33 changed files with 1945 additions and 35 deletions

View File

@ -31,4 +31,13 @@ public interface RouteHandler extends Adapter {
boolean canHandle(String method, String path) throws IOException;
void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io)
throws IOException;
default String getSanitizedPath(String path) {
// remove query params if exists
int qIdx = path.indexOf('?');
if (qIdx != -1) {
return path.substring(0, qIdx);
}
return path;
}
}

View File

@ -17,19 +17,36 @@
package org.apache.cloudstack.veeam;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import org.apache.cloudstack.veeam.filter.BasicAuthFilter;
import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
import org.apache.cloudstack.veeam.api.ApiService;
import org.apache.cloudstack.veeam.filter.BearerOrBasicAuthFilter;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.jetbrains.annotations.NotNull;
public class VeeamControlServer {
private static final Logger LOGGER = LogManager.getLogger(VeeamControlServer.class);
@ -49,6 +66,13 @@ public class VeeamControlServer {
return;
}
final String keystorePath = ServerPropertiesUtil.getKeystoreFile();
final String keystorePassword = ServerPropertiesUtil.getKeystorePassword();
final String keyManagerPassword = ServerPropertiesUtil.getKeystorePassword();
final boolean sslConfigured = StringUtils.isNotEmpty(keystorePath) &&
StringUtils.isNotEmpty(keystorePassword) &&
StringUtils.isNotEmpty(keyManagerPassword) &&
Files.exists(Paths.get(keystorePath));
final String bind = VeeamControlService.BindAddress.value();
final int port = VeeamControlService.Port.value();
String ctxPath = VeeamControlService.ContextPath.value();
@ -56,28 +80,119 @@ public class VeeamControlServer {
routeHandlers != null ? routeHandlers.size() : 0);
server = new Server(new InetSocketAddress(bind, port));
server = new Server();
if (sslConfigured) {
final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(keystorePath);
sslContextFactory.setKeyStorePassword(keystorePassword);
sslContextFactory.setKeyManagerPassword(keyManagerPassword);
final HttpConfiguration https = new HttpConfiguration();
https.setSecureScheme("https");
https.setSecurePort(port);
https.addCustomizer(new SecureRequestCustomizer());
final ServerConnector httpsConnector = new ServerConnector(
server,
new SslConnectionFactory(sslContextFactory, "http/1.1"),
new HttpConnectionFactory(https)
);
httpsConnector.setHost(bind);
httpsConnector.setPort(port);
server.addConnector(httpsConnector);
LOGGER.info("Veeam Control API server HTTPS enabled on {}:{}", bind, port);
} else {
final HttpConfiguration http = new HttpConfiguration();
final ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(http));
httpConnector.setHost(bind);
httpConnector.setPort(port);
server.addConnector(httpConnector);
LOGGER.warn("Veeam Control API server HTTPS is NOT configured (missing keystore path/passwords). " +
"Starting HTTP on {}:{} instead.", bind, port);
}
final ServletContextHandler ctx =
new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
ctx.setContextPath(ctxPath);
// Basic Auth for all routes
ctx.addFilter(BasicAuthFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Bearer or Basic Auth for all routes
ctx.addFilter(BearerOrBasicAuthFilter.class, ApiService.BASE_ROUTE + "/*", EnumSet.of(DispatcherType.REQUEST));
// Front controller servlet
ctx.addServlet(new ServletHolder(new VeeamControlServlet(routeHandlers)), "/*");
server.setHandler(ctx);
// Create a RequestLog that logs every request handled by the server (all contexts/paths)
server.setHandler(buildContextHandler(ctx));
server.start();
LOGGER.info("Started Veeam Control API server on {}:{} with context {}", bind, port, ctxPath);
}
@NotNull
private static Handler buildContextHandler(ServletContextHandler ctx) {
// Handler for root ('/') path
final ServletContextHandler root = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
root.setContextPath("/");
root.addServlet(new ServletHolder(new javax.servlet.http.HttpServlet() {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp)
throws java.io.IOException {
resp.setContentType("text/plain");
resp.setStatus(javax.servlet.http.HttpServletResponse.SC_OK);
resp.getWriter().println("Veeam Control API");
}
@Override
protected void doPost(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp)
throws java.io.IOException {
doGet(req, resp);
}
}), "/*");
final RequestLog requestLog = (request, response) -> {
final String uri = request.getRequestURI() +
(request.getQueryString() != null ? "?" + request.getQueryString() : "");
LOGGER.info("Request - remoteAddr: {}, method: {}, uri: {}, headers: {}, status: {}",
request.getRemoteAddr(),
request.getMethod(),
uri,
dumpRequestHeaders(request),
response.getStatus());
};
final RequestLogHandler requestLogHandler = new RequestLogHandler();
requestLogHandler.setRequestLog(requestLog);
// Attach both the configured context and the root handler; keep ctx first so contextPath has priority
final HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[] { ctx, root });
requestLogHandler.setHandler(handlers);
return requestLogHandler;
}
public void stop() throws Exception {
if (server != null) {
server.stop();
server = null;
}
}
private static String dumpRequestHeaders(HttpServletRequest request) {
final StringBuilder sb = new StringBuilder();
final Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
final String name = headerNames.nextElement();
final Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
sb.append(name).append("=").append(values.nextElement()).append("; ");
}
}
return sb.toString();
}
}

View File

@ -30,7 +30,7 @@ public interface VeeamControlService extends PluggableService, Configurable {
ConfigKey<Integer> Port = new ConfigKey<>("Advanced", Integer.class, "integration.veeam.control.port",
"8090", "Port for Veeam Integration REST API server", false);
ConfigKey<String> ContextPath = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.context.path",
"/integrations/veeam", "Context path for Veeam Integration REST API server", false);
"/ovirt-engine", "Context path for Veeam Integration REST API server", false);
ConfigKey<String> Username = new ConfigKey<>("Advanced", String.class, "integration.veeam.api.username",
"veeam", "Username for Basic Auth on Veeam Integration REST API server", true);
ConfigKey<String> Password = new ConfigKey<>("Advanced", String.class, "integration.veeam.api.password",

View File

@ -58,6 +58,34 @@ public class VeeamControlServlet extends HttpServlet {
LOGGER.info("Received {} request for {} with out format: {}", method, path, outFormat);
// Add a log to give all info about the request
try {
StringBuilder details = new StringBuilder();
details.append("Request details: Method: ").append(method).append(", Path: ").append(path);
details.append(", Query: ").append(req.getQueryString() == null ? "" : req.getQueryString());
details.append(", Headers: ");
java.util.Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames != null && headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
details.append(name).append("=").append(req.getHeader(name)).append("; ");
}
// String body = "";
// if (!"GET".equalsIgnoreCase(method)) {
// StringBuilder bodySb = new StringBuilder();
// java.io.BufferedReader reader = req.getReader();
// if (reader != null) {
// String line;
// while ((line = reader.readLine()) != null) {
// bodySb.append(line).append('\n');
// }
// }
// body = bodySb.toString().trim();
// }
// details.append(", Body: ").append(body);
LOGGER.debug(details.toString());
} catch (Exception e) {
LOGGER.debug("Failed to capture request details", e);
}
try {
if ("/".equals(path)) {

View File

@ -18,12 +18,28 @@
package org.apache.cloudstack.veeam.api;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.RouteHandler;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.VeeamControlServlet;
import org.apache.cloudstack.veeam.api.dto.Api;
import org.apache.cloudstack.veeam.api.dto.EmptyElement;
import org.apache.cloudstack.veeam.api.dto.Link;
import org.apache.cloudstack.veeam.api.dto.ProductInfo;
import org.apache.cloudstack.veeam.api.dto.Ref;
import org.apache.cloudstack.veeam.api.dto.SpecialObjectRef;
import org.apache.cloudstack.veeam.api.dto.SpecialObjects;
import org.apache.cloudstack.veeam.api.dto.Summary;
import org.apache.cloudstack.veeam.api.dto.SummaryCount;
import org.apache.cloudstack.veeam.api.dto.Version;
import org.apache.cloudstack.veeam.utils.Negotiation;
import com.cloud.utils.component.ManagerBase;
@ -33,12 +49,101 @@ public class ApiService extends ManagerBase implements RouteHandler {
@Override
public boolean canHandle(String method, String path) {
return path.startsWith("/api");
return getSanitizedPath(path).startsWith("/api");
}
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
// ToDo: handle root API requests
final String sanitizedPath = getSanitizedPath(path);
if (sanitizedPath.equals(BASE_ROUTE)) {
handleRootApiRequest(req, resp, outFormat, io);
return;
}
io.getWriter().writeFault(resp, HttpServletResponse.SC_NOT_FOUND, "Not found", null, outFormat);
}
private void handleRootApiRequest(HttpServletRequest req, HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
io.getWriter().write(resp, 200,
createDummyApi(VeeamControlService.ContextPath.value() + BASE_ROUTE),
outFormat);
}
private static Api createDummyApi(String basePath) {
Api api = new Api();
/* ---------------- Links ---------------- */
List<Link> links = new ArrayList<>();
add(links, basePath + "/clusters", "clusters");
add(links, basePath + "/clusters?search={query}", "clusters/search");
add(links, basePath + "/datacenters", "datacenters");
add(links, basePath + "/datacenters?search={query}", "datacenters/search");
add(links, basePath + "/events", "events");
add(links, basePath + "/events;from={event_id}?search={query}", "events/search");
add(links, basePath + "/hosts", "hosts");
add(links, basePath + "/hosts?search={query}", "hosts/search");
add(links, basePath + "/networks", "networks");
add(links, basePath + "/networks?search={query}", "networks/search");
add(links, basePath + "/storagedomains", "storagedomains");
add(links, basePath + "/storagedomains?search={query}", "storagedomains/search");
add(links, basePath + "/templates", "templates");
add(links, basePath + "/templates?search={query}", "templates/search");
add(links, basePath + "/vms", "vms");
add(links, basePath + "/vms?search={query}", "vms/search");
add(links, basePath + "/disks", "disks");
add(links, basePath + "/disks?search={query}", "disks/search");
api.link = links;
/* ---------------- Engine backup ---------------- */
api.engineBackup = new EmptyElement();
/* ---------------- Product info ---------------- */
ProductInfo productInfo = new ProductInfo();
productInfo.instanceId = UUID.randomUUID().toString();
productInfo.name = "oVirt Engine";
Version version = new Version();
version.build = "8";
version.fullVersion = "4.5.8-0.master.fake.el9";
version.major = 4;
version.minor = 5;
version.revision = 0;
productInfo.version = version;
api.productInfo = productInfo;
/* ---------------- Special objects ---------------- */
SpecialObjects specialObjects = new SpecialObjects();
specialObjects.blankTemplate = new SpecialObjectRef(
basePath + "/templates/00000000-0000-0000-0000-000000000000",
"00000000-0000-0000-0000-000000000000"
);
specialObjects.rootTag = new SpecialObjectRef(
basePath + "/tags/00000000-0000-0000-0000-000000000000",
"00000000-0000-0000-0000-000000000000"
);
api.specialObjects = specialObjects;
/* ---------------- Summary ---------------- */
Summary summary = new Summary();
summary.hosts = new SummaryCount(1, 1);
summary.storageDomains = new SummaryCount(1, 2);
summary.users = new SummaryCount(1, 1);
summary.vms = new SummaryCount(1, 8);
api.summary = summary;
/* ---------------- Time ---------------- */
api.time = OffsetDateTime.now(ZoneOffset.ofHours(2)).toInstant().toEpochMilli();
/* ---------------- Users ---------------- */
String userId = UUID.randomUUID().toString();
api.authenticatedUser = Ref.of(basePath + "/users/" + userId, userId);
api.effectiveUser = Ref.of(basePath + "/users/" + userId, userId);
return api;
}
private static void add(List<Link> links, String href, String rel) {
links.add(new Link(href, rel));
}
}

View File

@ -0,0 +1,184 @@
// 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.veeam.api;
import java.io.IOException;
import java.util.List;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.RouteHandler;
import org.apache.cloudstack.veeam.VeeamControlServlet;
import org.apache.cloudstack.veeam.api.converter.DataCenterVOToDataCenterConverter;
import org.apache.cloudstack.veeam.api.converter.StoreVOToStorageDomainConverter;
import org.apache.cloudstack.veeam.api.dto.DataCenter;
import org.apache.cloudstack.veeam.api.dto.DataCenters;
import org.apache.cloudstack.veeam.api.dto.StorageDomain;
import org.apache.cloudstack.veeam.api.dto.StorageDomains;
import org.apache.cloudstack.veeam.api.request.VmListQuery;
import org.apache.cloudstack.veeam.utils.Negotiation;
import org.apache.cloudstack.veeam.utils.PathUtil;
import com.cloud.api.query.dao.ImageStoreJoinDao;
import com.cloud.api.query.dao.StoragePoolJoinDao;
import com.cloud.api.query.vo.ImageStoreJoinVO;
import com.cloud.api.query.vo.StoragePoolJoinVO;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.utils.Pair;
import com.cloud.utils.component.ManagerBase;
public class DataCentersRouteHandler extends ManagerBase implements RouteHandler {
public static final String BASE_ROUTE = "/api/datacenters";
private static final int DEFAULT_MAX = 50;
private static final int HARD_CAP_MAX = 1000;
private static final int DEFAULT_PAGE = 1;
@Inject
DataCenterDao dataCenterDao;
@Inject
StoragePoolJoinDao storagePoolJoinDao;
@Inject
ImageStoreJoinDao imageStoreJoinDao;
@Override
public boolean start() {
return true;
}
@Override
public int priority() {
return 5;
}
@Override
public boolean canHandle(String method, String path) {
return getSanitizedPath(path).startsWith(BASE_ROUTE);
}
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
final String method = req.getMethod();
if (!"GET".equalsIgnoreCase(method)) {
io.methodNotAllowed(resp, "GET", outFormat);
return;
}
final String sanitizedPath = getSanitizedPath(path);
if (sanitizedPath.equals(BASE_ROUTE)) {
handleGet(req, resp, outFormat, io);
return;
}
Pair<String, String> idAndSubPath = PathUtil.extractIdAndSubPath(sanitizedPath, BASE_ROUTE);
if (idAndSubPath != null) {
// /api/datacenters/{id}
if (idAndSubPath.first() != null) {
if (idAndSubPath.second() == null) {
handleGetById(idAndSubPath.first(), resp, outFormat, io);
return;
}
if ("storagedomains".equals(idAndSubPath.second())) {
handleGetStorageDomainsByDcId(idAndSubPath.first(), resp, outFormat, io);
return;
}
}
}
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found");
}
/**
* Matches /api/datacenters/{id} where {id} is a single path segment (no extra '/').
* Returns id or null.
*/
private static String matchSinglePathParam(final String path, final String prefix) {
if (!path.startsWith(prefix)) return null;
final String rest = path.substring(prefix.length()); // after "/api/datacenters/"
if (rest.isEmpty()) return null;
if (rest.contains("/")) return null; // ensure only 1 segment
return rest;
}
public void handleGet(final HttpServletRequest req, final HttpServletResponse resp,
Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
final List<DataCenter> result = DataCenterVOToDataCenterConverter.toDCList(listDCs());
final DataCenters response = new DataCenters(result);
io.getWriter().write(resp, 200, response, outFormat);
}
private static VmListQuery fromRequest(final HttpServletRequest req) {
final VmListQuery q = new VmListQuery();
q.setSearch(req.getParameter("search"));
q.setMax(parseIntOrNull(req.getParameter("max")));
q.setPage(parseIntOrNull(req.getParameter("page")));
return q;
}
private static Integer parseIntOrNull(final String s) {
if (s == null || s.trim().isEmpty()) return null;
try {
return Integer.parseInt(s.trim());
} catch (NumberFormatException e) {
return Integer.valueOf(-1); // will be rejected by validation above
}
}
protected List<DataCenterVO> listDCs() {
return dataCenterDao.listAll();
}
public void handleGetById(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat,
final VeeamControlServlet io) throws IOException {
final DataCenterVO dataCenterVO = dataCenterDao.findByUuid(id);
if (dataCenterVO == null) {
io.notFound(resp, "DataCenter not found: " + id, outFormat);
return;
}
DataCenter response = DataCenterVOToDataCenterConverter.toDataCenter(dataCenterVO);
io.getWriter().write(resp, 200, response, outFormat);
}
protected List<StoragePoolJoinVO> listStoragePoolsByDcId(final long dcId) {
return storagePoolJoinDao.listAll();
}
protected List<ImageStoreJoinVO> listImageStoresByDcId(final long dcId) {
return imageStoreJoinDao.listAll();
}
public void handleGetStorageDomainsByDcId(final String id, final HttpServletResponse resp, final Negotiation.OutFormat outFormat,
final VeeamControlServlet io) throws IOException {
final DataCenterVO dataCenterVO = dataCenterDao.findByUuid(id);
if (dataCenterVO == null) {
io.notFound(resp, "DataCenter not found: " + id, outFormat);
return;
}
List<StorageDomain> storageDomains = StoreVOToStorageDomainConverter.toStorageDomainListFromPools(listStoragePoolsByDcId(dataCenterVO.getId()));
storageDomains.addAll(StoreVOToStorageDomainConverter.toStorageDomainListFromStores(listImageStoresByDcId(dataCenterVO.getId())));
StorageDomains response = new StorageDomains(storageDomains);
io.getWriter().write(resp, 200, response, outFormat);
}
}

View File

@ -68,13 +68,14 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler {
@Override
public boolean canHandle(String method, String path) {
return path.startsWith(BASE_ROUTE);
return getSanitizedPath(path).startsWith(BASE_ROUTE);
}
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
final String method = req.getMethod();
if (path.equals(BASE_ROUTE)) {
final String sanitizedPath = getSanitizedPath(path);
if (sanitizedPath.equals(BASE_ROUTE)) {
if (!"GET".equalsIgnoreCase(method)) {
io.methodNotAllowed(resp, "GET", outFormat);
return;
@ -84,7 +85,7 @@ public class VmsRouteHandler extends ManagerBase implements RouteHandler {
}
// /api/vms/{id}
final String vmId = matchSinglePathParam(path, BASE_ROUTE + "/");
final String vmId = matchSinglePathParam(sanitizedPath, BASE_ROUTE + "/");
if (vmId != null) {
if (!"GET".equalsIgnoreCase(method)) {
io.methodNotAllowed(resp, "GET", outFormat);

View File

@ -0,0 +1,80 @@
// 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.veeam.api.converter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.api.DataCentersRouteHandler;
import org.apache.cloudstack.veeam.api.dto.DataCenter;
import org.apache.cloudstack.veeam.api.dto.Link;
import org.apache.cloudstack.veeam.api.dto.Ref;
import org.apache.cloudstack.veeam.api.dto.SupportedVersions;
import org.apache.cloudstack.veeam.api.dto.Version;
import com.cloud.dc.DataCenterVO;
import com.cloud.org.Grouping;
public class DataCenterVOToDataCenterConverter {
public static DataCenter toDataCenter(final DataCenterVO zone) {
final String id = zone.getUuid();
final String basePath = VeeamControlService.ContextPath.value();
final String href = basePath + DataCentersRouteHandler.BASE_ROUTE + "/datacenters/" + id;
final DataCenter dc = new DataCenter();
// ---- Identity ----
dc.id = id;
dc.href = href;
dc.name = zone.getName();
dc.description = zone.getDescription();
// ---- State ----
dc.status = Grouping.AllocationState.Enabled.equals(zone.getAllocationState()) ? "up" : "down";
dc.local = "false";
dc.quotaMode = "disabled";
dc.storageFormat = "v5";
// ---- Versions (static but valid) ----
final Version v48 = new Version();
v48.major = 4;
v48.minor = 8;
dc.version = v48;
dc.supportedVersions = new SupportedVersions(List.of(v48));
// ---- mac_pool (static placeholder) ----
dc.macPool = Ref.of(basePath + "/macpools/default","default");
// ---- Related links ----
dc.link = Arrays.asList(
new Link(href + "/clusters", "clusters"),
new Link(href + "/networks", "networks"),
new Link(href + "/storagedomains", "storagedomains")
);
return dc;
}
public static List<DataCenter> toDCList(final List<DataCenterVO> srcList) {
return srcList.stream()
.map(DataCenterVOToDataCenterConverter::toDataCenter)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,248 @@
// 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.veeam.api.converter;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.api.ApiService;
import org.apache.cloudstack.veeam.api.DataCentersRouteHandler;
import org.apache.cloudstack.veeam.api.dto.DataCenter;
import org.apache.cloudstack.veeam.api.dto.DataCenters;
import org.apache.cloudstack.veeam.api.dto.Link;
import org.apache.cloudstack.veeam.api.dto.Storage;
import org.apache.cloudstack.veeam.api.dto.StorageDomain;
import com.cloud.api.query.vo.ImageStoreJoinVO;
import com.cloud.api.query.vo.StoragePoolJoinVO;
public class StoreVOToStorageDomainConverter {
/** Primary storage -> oVirt storage_domain (type=data) */
public static StorageDomain toStorageDomain(final StoragePoolJoinVO pool) {
final String basePath = VeeamControlService.ContextPath.value();
final String id = pool.getUuid();
StorageDomain sd = new StorageDomain();
sd.id = id;
final String href = href(basePath, ApiService.BASE_ROUTE + "/storagedomains/" + id);
sd.href = href;
sd.name = pool.getName();
sd.description = ""; // oVirt often returns empty string
sd.comment = "";
// oVirt sample returns numbers as strings
sd.available = Long.toString(pool.getCapacityBytes() - pool.getUsedBytes());
sd.used = Long.toString(pool.getUsedBytes());
sd.committed = Long.toString(pool.getCapacityBytes());
sd.type = "data";
sd.status = mapPoolStatus(pool); // "active"/"inactive"/"maintenance" (approx)
sd.master = "true"; // if you dont have a concept, choose stable default
sd.backup = "false";
sd.blockSize = "512"; // stable default unless you can compute it
sd.externalStatus = "ok";
sd.storageFormat = "v5";
sd.discardAfterDelete = "false";
sd.wipeAfterDelete = "false";
sd.supportsDiscard = "false";
sd.supportsDiscardZeroesData = "false";
sd.warningLowSpaceIndicator = "10";
sd.criticalSpaceActionBlocker = "5";
// Nested storage (try to extract if available)
sd.storage = buildPrimaryStorage(pool);
// dc attachment
String dcId = pool.getZoneUuid();
DataCenter dc = new DataCenter();
dc.href = href(basePath, DataCentersRouteHandler.BASE_ROUTE + dcId);
dc.id = dcId;
sd.dataCenters = new DataCenters(List.of(dc));
sd.link = defaultStorageDomainLinks(href, true, /*includeTemplates*/ true);
return sd;
}
public static List<StorageDomain> toStorageDomainListFromPools(final List<StoragePoolJoinVO> pools) {
return pools.stream().map(StoreVOToStorageDomainConverter::toStorageDomain).collect(Collectors.toList());
}
/** Secondary/Image store -> oVirt storage_domain (type=image) */
public static StorageDomain toStorageDomain(final ImageStoreJoinVO store) {
final String basePath = VeeamControlService.ContextPath.value();
final String id = store.getUuid();
StorageDomain sd = new StorageDomain();
sd.id = id;
final String href = href(basePath, ApiService.BASE_ROUTE + "/storagedomains/" + id);
sd.href = href;
sd.name = store.getName();
sd.description = "";
sd.comment = "";
// Many image repos dont have these values readily; keep as "0" or omit (null)
sd.committed = "0";
sd.available = null; // oVirts glance example omitted available/used
sd.used = null;
sd.type = "image";
sd.status = "unattached"; // matches your sample for glance-like repo
sd.master = "false";
sd.backup = "false";
sd.blockSize = "512";
sd.externalStatus = "ok";
sd.storageFormat = "v1";
sd.discardAfterDelete = "false";
sd.wipeAfterDelete = "false";
sd.supportsDiscard = "false";
sd.supportsDiscardZeroesData = "false";
sd.warningLowSpaceIndicator = "0";
sd.criticalSpaceActionBlocker = "0";
sd.storage = buildImageStoreStorage(store);
// Optionally include dc attachment (your first object had it; second didnt)
String dcId = store.getZoneUuid();
DataCenter dc = new DataCenter();
dc.href = href(basePath, DataCentersRouteHandler.BASE_ROUTE + dcId);
dc.id = dcId;
sd.dataCenters = new DataCenters(List.of(dc));
sd.link = defaultStorageDomainLinks(href, false, /*includeTemplates*/ false);
return sd;
}
public static List<StorageDomain> toStorageDomainListFromStores(final List<ImageStoreJoinVO> stores) {
return stores.stream().map(StoreVOToStorageDomainConverter::toStorageDomain).collect(Collectors.toList());
}
// ----------- Helpers -----------
private static Storage buildPrimaryStorage(StoragePoolJoinVO pool) {
Storage st = new Storage();
st.type = mapPrimaryStorageType(pool);
// If you can parse details/url, fill these. If not, keep empty strings like oVirt.
// For NFS pools in CloudStack, URL is often like: nfs://10.0.32.4/path or 10.0.32.4:/path
String url = null;
try {
url = pool.getHostAddress(); // sometimes exists in VO; if not, ignore
} catch (Exception ignored) { }
if ("nfs".equals(st.type)) {
// best-effort placeholders
st.address = ""; // fill if you can parse
st.path = ""; // fill if you can parse
st.mountOptions = "";
st.nfsVersion = "auto";
}
return st;
}
private static Storage buildImageStoreStorage(ImageStoreJoinVO store) {
Storage st = new Storage();
// Match your sample: glance store => type=glance
// If you want "nfs" for secondary, map based on provider/protocol instead.
st.type = mapImageStorageType(store);
if ("nfs".equals(st.type)) {
st.address = "";
st.path = "";
st.mountOptions = "";
st.nfsVersion = "auto";
}
return st;
}
private static List<Link> defaultStorageDomainLinks(String basePath, boolean includeDisks, boolean includeTemplates) {
// Mirrors the rels you pasted; keep stable order.
// You can add/remove based on what endpoints you actually implement.
List<Link> common = new java.util.ArrayList<>();
common.add(new Link("diskprofiles", href(basePath, "/diskprofiles")));
if (includeDisks) {
common.add(new Link("disks", href(basePath, "/disks")));
common.add(new Link("storageconnections", href(basePath, "/storageconnections")));
}
common.add(new Link("permissions", href(basePath, "/permissions")));
if (includeTemplates) {
common.add(new Link("templates", href(basePath, "/templates")));
common.add(new Link("vms", href(basePath, "/vms")));
} else {
common.add(new Link("images", href(basePath, "/images")));
}
common.add(new Link("disksnapshots", href(basePath, "/disksnapshots")));
return common;
}
private static String mapPoolStatus(StoragePoolJoinVO pool) {
// This is approximate; adjust if you have better signals.
try {
Object status = pool.getStatus(); // often StoragePoolStatus enum
if (status != null) {
String s = status.toString().toLowerCase();
if (s.contains("up") || s.contains("enabled")) return "active";
if (s.contains("maintenance")) return "maintenance";
}
} catch (Exception ignored) { }
return "inactive";
}
private static String mapPrimaryStorageType(StoragePoolJoinVO pool) {
try {
Object t = pool.getPoolType(); // often StoragePoolType enum
if (t != null) {
String s = t.toString().toLowerCase();
if (s.contains("networkfilesystem") || s.contains("nfs")) return "nfs";
if (s.contains("iscsi")) return "iscsi";
if (s.contains("filesystem")) return "posixfs";
if (s.contains("rbd") || s.contains("ceph")) return "cinder"; // not perfect; pick stable
}
} catch (Exception ignored) { }
return "unknown";
}
private static String mapImageStorageType(ImageStoreJoinVO store) {
// If your secondary store is S3/NFS/etc, you may want different mapping.
// For your oVirt sample, "glance" is used for an image repo.
try {
String provider = store.getProviderName(); // may exist
if (provider != null && provider.toLowerCase().contains("glance")) return "glance";
} catch (Exception ignored) { }
return "glance";
}
private static String href(String baseUrl, String path) {
if (baseUrl.endsWith("/")) baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
return baseUrl + path;
}
}

View File

@ -25,7 +25,10 @@ import java.util.stream.Collectors;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.api.ApiService;
import org.apache.cloudstack.veeam.api.VmsRouteHandler;
import org.apache.cloudstack.veeam.api.dto.Bios;
import org.apache.cloudstack.veeam.api.dto.Cpu;
import org.apache.cloudstack.veeam.api.dto.Link;
import org.apache.cloudstack.veeam.api.dto.Os;
import org.apache.cloudstack.veeam.api.dto.Ref;
import org.apache.cloudstack.veeam.api.dto.Topology;
import org.apache.cloudstack.veeam.api.dto.Vm;
@ -76,13 +79,25 @@ public final class UserVmJoinVOToVmConverter {
basePath + ApiService.BASE_ROUTE,
"cluster",
src.getHostUuid());
dst.memory = (long) src.getRamSize();
dst.memory = src.getRamSize() * 1024L * 1024L;
dst.cpu = new Cpu(src.getArch(), new Topology(src.getCpu(), 1, 1));
dst.os = null;
dst.bios = null;
dst.actions = null;
dst.link = null;
dst.os = new Os();
dst.os.type = src.getGuestOsId() % 2 == 0
? "windows"
: "linux";
dst.bios = new Bios();
dst.bios.type = "legacy";
dst.type = "server";
dst.origin = "ovirt";
dst.actions = null;dst.link = List.of(
new Link("diskattachments",
dst.href + "/diskattachments"),
new Link("nics",
dst.href + "/nics"),
new Link("snapshots",
dst.href + "/snapshots")
);
return dst;
}

View File

@ -0,0 +1,62 @@
// 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.veeam.api.dto;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
/**
* Root response for GET /ovirt-engine/api
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlRootElement(localName = "api")
public final class Api {
// <link .../> repeated
@JacksonXmlElementWrapper(useWrapping = false)
public List<Link> link;
// <engine_backup/> (empty element)
@JacksonXmlProperty(localName = "engine_backup")
public EmptyElement engineBackup;
@JacksonXmlProperty(localName = "product_info")
public ProductInfo productInfo;
@JacksonXmlProperty(localName = "special_objects")
public SpecialObjects specialObjects;
@JacksonXmlProperty(localName = "summary")
public Summary summary;
// Keep as String to avoid timezone/date parsing friction; you control formatting.
@JacksonXmlProperty(localName = "time")
public Long time;
@JacksonXmlProperty(localName = "authenticated_user")
public Ref authenticatedUser;
@JacksonXmlProperty(localName = "effective_user")
public Ref effectiveUser;
public Api() {}
}

View File

@ -0,0 +1,62 @@
// 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.veeam.api.dto;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlRootElement(localName = "data_center")
public final class DataCenter {
// keep strings to match oVirt JSON ("false", "disabled", "up", "v5", etc.)
public String local;
@JsonProperty("quota_mode")
public String quotaMode;
public String status;
@JsonProperty("storage_format")
public String storageFormat;
@JsonProperty("supported_versions")
public SupportedVersions supportedVersions;
public Version version;
@JsonProperty("mac_pool")
public Ref macPool;
public Actions actions;
public String name;
public String description;
@JacksonXmlElementWrapper(useWrapping = false)
public List<Link> link;
public String href;
public String id;
public DataCenter() {}
}

View File

@ -0,0 +1,48 @@
// 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.veeam.api.dto;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
/**
* Root collection wrapper:
* {
* "data_center": [ { ... } ]
* }
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlRootElement(localName = "data_centers")
@JsonPropertyOrder({ "data_center" })
public final class DataCenters {
@JsonProperty("data_center")
@JacksonXmlElementWrapper(useWrapping = false)
public List<DataCenter> dataCenter;
public DataCenters() {}
public DataCenters(final List<DataCenter> dataCenter) {
this.dataCenter = dataCenter;
}
}

View File

@ -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.veeam.api.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = EmptyElementSerializer.class)
public final class EmptyElement {
public EmptyElement() {}
}

View File

@ -0,0 +1,37 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
* Serializes as an "empty object".
* With Jackson XML this becomes an empty element: <engine_backup/>.
*/
public final class EmptyElementSerializer extends JsonSerializer<EmptyElement> {
@Override
public void serialize(EmptyElement value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeEndObject();
}
}

View File

@ -0,0 +1,36 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class ProductInfo {
@JacksonXmlProperty(localName = "instance_id")
public String instanceId;
@JacksonXmlProperty(localName = "name")
public String name;
@JacksonXmlProperty(localName = "version")
public Version version;
public ProductInfo() {}
}

View File

@ -0,0 +1,38 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class SpecialObjectRef {
@JacksonXmlProperty(isAttribute = true, localName = "href")
public String href;
@JacksonXmlProperty(isAttribute = true, localName = "id")
public String id;
public SpecialObjectRef() {}
public SpecialObjectRef(String href, String id) {
this.href = href;
this.id = id;
}
}

View File

@ -0,0 +1,33 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class SpecialObjects {
@JacksonXmlProperty(localName = "blank_template")
public SpecialObjectRef blankTemplate;
@JacksonXmlProperty(localName = "root_tag")
public SpecialObjectRef rootTag;
public SpecialObjects() {}
}

View File

@ -0,0 +1,42 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class Storage {
public String type; // nfs / glance
// nfs-ish fields (optional)
public String address;
public String path;
@JsonProperty("mount_options")
@JacksonXmlProperty(localName = "mount_options")
public String mountOptions;
@JsonProperty("nfs_version")
@JacksonXmlProperty(localName = "nfs_version")
public String nfsVersion;
public Storage() {}
}

View File

@ -0,0 +1,100 @@
// 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.veeam.api.dto;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlRootElement(localName = "storage_domain")
public final class StorageDomain {
// Identifiers
public String id;
public String href;
public String name;
public String description;
public String comment;
// oVirt returns these as strings in your sample
public String available;
public String used;
public String committed;
@JsonProperty("block_size")
@JacksonXmlProperty(localName = "block_size")
public String blockSize;
@JsonProperty("warning_low_space_indicator")
@JacksonXmlProperty(localName = "warning_low_space_indicator")
public String warningLowSpaceIndicator;
@JsonProperty("critical_space_action_blocker")
@JacksonXmlProperty(localName = "critical_space_action_blocker")
public String criticalSpaceActionBlocker;
public String status; // e.g. "unattached" (optional in your first object)
public String type; // data / image / iso / export
public String master; // "true"/"false"
public String backup; // "true"/"false"
@JsonProperty("external_status")
@JacksonXmlProperty(localName = "external_status")
public String externalStatus; // "ok"
@JsonProperty("storage_format")
@JacksonXmlProperty(localName = "storage_format")
public String storageFormat; // v5 / v1
@JsonProperty("discard_after_delete")
@JacksonXmlProperty(localName = "discard_after_delete")
public String discardAfterDelete;
@JsonProperty("wipe_after_delete")
@JacksonXmlProperty(localName = "wipe_after_delete")
public String wipeAfterDelete;
@JsonProperty("supports_discard")
@JacksonXmlProperty(localName = "supports_discard")
public String supportsDiscard;
@JsonProperty("supports_discard_zeroes_data")
@JacksonXmlProperty(localName = "supports_discard_zeroes_data")
public String supportsDiscardZeroesData;
// Nested
public Storage storage;
@JsonProperty("data_centers")
@JacksonXmlProperty(localName = "data_centers")
public DataCenters dataCenters;
public Actions actions;
@JacksonXmlElementWrapper(useWrapping = false)
public List<Link> link;
public StorageDomain() {}
}

View File

@ -0,0 +1,39 @@
// 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.veeam.api.dto;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JacksonXmlRootElement(localName = "storage_domains")
public final class StorageDomains {
@JsonProperty("storage_domain")
@JacksonXmlElementWrapper(useWrapping = false)
public List<StorageDomain> storageDomain;
public StorageDomains() {}
public StorageDomains(List<StorageDomain> storageDomain) {
this.storageDomain = storageDomain;
}
}

View File

@ -0,0 +1,39 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class Summary {
@JacksonXmlProperty(localName = "hosts")
public SummaryCount hosts;
@JacksonXmlProperty(localName = "storage_domains")
public SummaryCount storageDomains;
@JacksonXmlProperty(localName = "users")
public SummaryCount users;
@JacksonXmlProperty(localName = "vms")
public SummaryCount vms;
public Summary() {}
}

View File

@ -0,0 +1,38 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class SummaryCount {
@JacksonXmlProperty(localName = "active")
public Integer active;
@JacksonXmlProperty(localName = "total")
public Integer total;
public SummaryCount() {}
public SummaryCount(Integer active, Integer total) {
this.active = active;
this.total = total;
}
}

View File

@ -0,0 +1,37 @@
// 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.veeam.api.dto;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class SupportedVersions {
@JsonProperty("version")
@JacksonXmlElementWrapper(useWrapping = false)
public List<Version> version;
public SupportedVersions() {}
public SupportedVersions(final List<Version> version) {
this.version = version;
}
}

View File

@ -0,0 +1,42 @@
// 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.veeam.api.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class Version {
@JacksonXmlProperty(localName = "build")
public String build;
@JacksonXmlProperty(localName = "full_version")
public String fullVersion;
@JacksonXmlProperty(localName = "major")
public Integer major;
@JacksonXmlProperty(localName = "minor")
public Integer minor;
@JacksonXmlProperty(localName = "revision")
public Integer revision;
public Version() {}
}

View File

@ -61,6 +61,10 @@ public final class Vm {
public Os os;
public Bios bios;
public boolean stateless; // true|false
public String type; // "server"
public String origin; // "ovirt"
public Actions actions; // actions.link[]
@JacksonXmlElementWrapper(useWrapping = false)
public List<Link> link; // related resources

View File

@ -0,0 +1,249 @@
// 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.veeam.filter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.VeeamControlService;
public class BearerOrBasicAuthFilter implements Filter {
// Keep these aligned with SsoService (move to ConfigKeys later)
public static final List<String> REQUIRED_SCOPES = List.of("ovirt-app-admin", "ovirt-app-portal");
public static final String ISSUER = "veeam-control";
private static final String HMAC_SECRET = "change-this-super-secret-key-change-this";
@Override public void init(FilterConfig filterConfig) {}
@Override public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest req = (HttpServletRequest) request;
final HttpServletResponse resp = (HttpServletResponse) response;
final String auth = req.getHeader("Authorization");
if (auth != null && auth.regionMatches(true, 0, "Bearer ", 0, 7)) {
final String token = auth.substring(7).trim();
if (token.isEmpty()) {
unauthorized(req, resp, "invalid_token", "Missing Bearer token");
return;
}
if (!verifyJwtHs256(token)) {
unauthorized(req, resp, "invalid_token", "Invalid or expired token");
return;
}
chain.doFilter(request, response);
return;
}
// Optional fallback: Basic (handy for manual testing).
if (auth != null && auth.regionMatches(true, 0, "Basic ", 0, 6)) {
if (!verifyBasic(auth.substring(6))) {
unauthorized(req, resp, "invalid_client", "Invalid Basic credentials");
return;
}
chain.doFilter(request, response);
return;
}
unauthorized(req, resp, "invalid_token", "Missing Authorization");
}
private boolean verifyBasic(String b64) {
final String expectedUser = VeeamControlService.Username.value();
final String expectedPass = VeeamControlService.Password.value();
final String decoded;
try {
decoded = new String(Base64.getDecoder().decode(b64), StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
return false;
}
final int idx = decoded.indexOf(':');
if (idx <= 0) return false;
final String user = decoded.substring(0, idx);
final String pass = decoded.substring(idx + 1);
return constantTimeEquals(user, expectedUser) && constantTimeEquals(pass, expectedPass);
}
/**
* Minimal JWT verification:
* - HS256 signature
* - "iss" matches
* - "exp" not expired
* - "scope" contains REQUIRED_SCOPES (space-separated)
*
* NOTE: This does not parse JSON robustly; its sufficient for the token you mint in SsoService.
* If you want robust parsing, switch to Nimbus and keep the rest the same.
*/
private boolean verifyJwtHs256(String token) {
final String[] parts = token.split("\\.");
if (parts.length != 3) return false;
final String headerB64 = parts[0];
final String payloadB64 = parts[1];
final String sigB64 = parts[2];
final byte[] expectedSig;
try {
expectedSig = hmacSha256((headerB64 + "." + payloadB64).getBytes(StandardCharsets.UTF_8),
HMAC_SECRET.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
return false;
}
final byte[] providedSig;
try {
providedSig = Base64.getUrlDecoder().decode(sigB64);
} catch (IllegalArgumentException e) {
return false;
}
if (!constantTimeEquals(expectedSig, providedSig)) return false;
final String payloadJson;
try {
payloadJson = new String(Base64.getUrlDecoder().decode(payloadB64), StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
return false;
}
// Super small claims extraction (good enough for our minting format)
final String iss = JsonMini.getString(payloadJson, "iss");
final String scope = JsonMini.getString(payloadJson, "scope");
final Long exp = JsonMini.getLong(payloadJson, "exp");
if (iss == null || !ISSUER.equals(iss)) return false;
if (exp == null || Instant.now().getEpochSecond() >= exp) return false;
if (scope == null || !hasRequiredScopes(scope)) return false;
return true;
}
private static boolean hasRequiredScopes(String scope) {
String[] scopes = scope.split("\\s+");
for (String required : REQUIRED_SCOPES) {
if (!hasScope(scopes, required)) return false;
}
return true;
}
private static boolean hasScope(String[] scopes, String required) {
for (String scope : scopes) {
if (scope.equals(required)) {
return true;
}
}
return false;
}
private static byte[] hmacSha256(byte[] data, byte[] key) throws Exception {
final Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
private static void unauthorized(HttpServletRequest req, HttpServletResponse resp,
String error, String desc) throws IOException {
// IMPORTANT: dont throw (your current filter throws and Jetty turns it into 500) :contentReference[oaicite:3]{index=3}
resp.resetBuffer();
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// Helpful for OAuth clients:
resp.setHeader("WWW-Authenticate",
"Bearer realm=\"Veeam Integration\", error=\"" + esc(error) + "\", error_description=\"" + esc(desc) + "\"");
final String accept = req.getHeader("Accept");
final boolean wantsJson = accept != null && accept.toLowerCase().contains("application/json");
resp.setCharacterEncoding("UTF-8");
if (wantsJson) {
resp.setContentType("application/json; charset=UTF-8");
resp.getWriter().write("{\"error\":\"" + esc(error) + "\",\"error_description\":\"" + esc(desc) + "\"}");
} else {
resp.setContentType("text/html; charset=UTF-8");
resp.getWriter().write("<html><head><title>Error</title></head><body>Unauthorized</body></html>");
}
resp.getWriter().flush();
}
private static String esc(String s) {
return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
}
private static boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
return constantTimeEquals(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
private static boolean constantTimeEquals(byte[] x, byte[] y) {
if (x.length != y.length) return false;
int r = 0;
for (int i = 0; i < x.length; i++) r |= x[i] ^ y[i];
return r == 0;
}
// Tiny JSON extractor for flat string/number claims. Good enough for tokens you mint.
static final class JsonMini {
static String getString(String json, String key) {
final String needle = "\"" + key + "\":";
int i = json.indexOf(needle);
if (i < 0) return null;
i += needle.length();
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
if (i >= json.length() || json.charAt(i) != '"') return null;
i++;
int j = json.indexOf('"', i);
if (j < 0) return null;
return json.substring(i, j);
}
static Long getLong(String json, String key) {
final String needle = "\"" + key + "\":";
int i = json.indexOf(needle);
if (i < 0) return null;
i += needle.length();
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
int j = i;
while (j < json.length() && (Character.isDigit(json.charAt(j)))) j++;
if (j == i) return null;
return Long.parseLong(json.substring(i, j));
}
}
}

View File

@ -18,27 +18,40 @@
package org.apache.cloudstack.veeam.sso;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.RouteHandler;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.VeeamControlServlet;
import org.apache.cloudstack.veeam.filter.BearerOrBasicAuthFilter;
import org.apache.cloudstack.veeam.utils.Negotiation;
import com.cloud.utils.component.ManagerBase;
public class SsoService extends ManagerBase implements RouteHandler {
private static final String BASE_ROUTE = "/sso";
private static final String HMAC_SECRET = "change-this-super-secret-key-change-this"; // >= 32 chars recommended
private static final long DEFAULT_TTL_SECONDS = 3600;
// Replace with your real credential validation (CloudStack account, config, etc.)
private final PasswordAuthenticator authenticator = new StaticPasswordAuthenticator();
@Override
public boolean canHandle(String method, String path) {
return path.startsWith("/sso");
return getSanitizedPath(path).startsWith(BASE_ROUTE);
}
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
if ("/sso/oauth/token".equals(path)) {
final String sanitizedPath = getSanitizedPath(path);
if (sanitizedPath.equals(BASE_ROUTE + "/oauth/token")) {
handleToken(req, resp, outFormat, io);
return;
}
@ -46,27 +59,127 @@ public class SsoService extends ManagerBase implements RouteHandler {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found");
}
protected void handleToken(HttpServletRequest req, HttpServletResponse resp, Negotiation.OutFormat outFormat, VeeamControlServlet io)
throws IOException {
protected void handleToken(HttpServletRequest req, HttpServletResponse resp,
Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
// Typically POST; you only listed 200/400/401 -> treat others as 400
if (!"POST".equals(req.getMethod())) {
throw VeeamControlServlet.Error.badRequest("token endpoint requires POST");
if (!"POST".equalsIgnoreCase(req.getMethod())) {
// oVirt-like: 405 is fine; if you strictly want 400, change to SC_BAD_REQUEST
resp.setHeader("Allow", "POST");
io.getWriter().write(resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED,
Map.of("error", "method_not_allowed", "message", "token endpoint requires POST"), outFormat);
return;
}
// Assume x-www-form-urlencoded for OAuth token requests (common)
String grantType = req.getParameter("grant_type");
if (grantType == null || grantType.isBlank()) {
throw VeeamControlServlet.Error.badRequest("Missing parameter: grant_type");
// OAuth password grant uses x-www-form-urlencoded. With servlet containers this usually populates getParameter().
final String grantType = trimToNull(req.getParameter("grant_type"));
final String scope = trimToNull(req.getParameter("scope")); // typically "ovirt-app-api"
final String username = trimToNull(req.getParameter("username"));
final String password = trimToNull(req.getParameter("password"));
if (grantType == null) {
io.getWriter().write(resp, HttpServletResponse.SC_BAD_REQUEST,
Map.of("error", "invalid_request", "error_description", "Missing parameter: grant_type"), outFormat);
return;
}
if (!"password".equals(grantType)) {
io.getWriter().write(resp, HttpServletResponse.SC_BAD_REQUEST,
Map.of("error", "unsupported_grant_type", "error_description", "Only grant_type=password is supported"), outFormat);
return;
}
if (username == null || password == null) {
io.getWriter().write(resp, HttpServletResponse.SC_BAD_REQUEST,
Map.of("error", "invalid_request", "error_description", "Missing username/password"), outFormat);
return;
}
// NOTE: 401 is normally handled by BasicAuthFilter; keep hook here if you later move auth here.
// if (!authorized) throw VeeamControlServlet.Error.unauthorized("Unauthorized");
if (!authenticator.authenticate(username, password)) {
// 401 for bad creds
io.getWriter().write(resp, HttpServletResponse.SC_UNAUTHORIZED,
Map.of("error", "invalid_grant", "error_description", "Invalid credentials"), outFormat);
return;
}
io.getWriter().write(resp, 200, Map.of(
"access_token", "dummy-token",
"token_type", "bearer",
"expires_in", 3600
), outFormat);
final String effectiveScope = (scope == null) ? "ovirt-app-api" : scope;
final long ttl = DEFAULT_TTL_SECONDS;
final String token;
try {
token = JwtUtil.issueHs256Jwt(BearerOrBasicAuthFilter.ISSUER, username, effectiveScope, ttl, HMAC_SECRET);
} catch (Exception e) {
io.getWriter().write(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
Map.of("error", "server_error", "error_description", "Failed to issue token"), outFormat);
return;
}
final Map<String, Object> payload = new HashMap<>();
payload.put("access_token", token);
payload.put("token_type", "bearer");
payload.put("expires_in", ttl);
payload.put("scope", effectiveScope);
io.getWriter().write(resp, HttpServletResponse.SC_OK, payload, outFormat);
}
private static String trimToNull(String s) {
if (s == null) return null;
s = s.trim();
return s.isEmpty() ? null : s;
}
// ---------- Minimal auth helpers (replace later) ----------
interface PasswordAuthenticator {
boolean authenticate(String username, String password);
}
static final class StaticPasswordAuthenticator implements PasswordAuthenticator {
StaticPasswordAuthenticator() {
}
@Override
public boolean authenticate(String username, String password) {
return VeeamControlService.Username.value().equals(username) &&
VeeamControlService.Password.value().equals(password);
}
}
// ---------- Minimal JWT HS256 without extra libs ----------
// (If you prefer Nimbus, I can convert this to nimbus-jose-jwt; this keeps dependencies tiny.)
static final class JwtUtil {
static String issueHs256Jwt(String issuer, String subject, String scope, long ttlSeconds, String secret) throws Exception {
long now = Instant.now().getEpochSecond();
long exp = now + ttlSeconds;
String headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
String payloadJson =
"{"
+ "\"iss\":\"" + jsonEscape(issuer) + "\","
+ "\"sub\":\"" + jsonEscape(subject) + "\","
+ "\"scope\":\"" + jsonEscape(scope) + "\","
+ "\"iat\":" + now + ","
+ "\"exp\":" + exp
+ "}";
String header = b64Url(headerJson.getBytes("UTF-8"));
String payload = b64Url(payloadJson.getBytes("UTF-8"));
String signingInput = header + "." + payload;
byte[] sig = hmacSha256(signingInput.getBytes("UTF-8"), secret.getBytes("UTF-8"));
return signingInput + "." + b64Url(sig);
}
static byte[] hmacSha256(byte[] data, byte[] key) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
static String b64Url(byte[] in) {
return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(in);
}
static String jsonEscape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
}

View File

@ -0,0 +1,62 @@
// 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.veeam.utils;
import com.cloud.utils.Pair;
public class PathUtil {
public static Pair<String, String> extractIdAndSubPath(final String path, final String baseRoute) {
// baseRoute = "/api/datacenters"
if (!path.startsWith(baseRoute)) {
return null;
}
// Remove base route
String rest = path.substring(baseRoute.length());
// Expect "" or "/{id}" or "/{id}/{sub}"
if (rest.isEmpty()) {
return null; // /api/datacenters (no id)
}
if (!rest.startsWith("/")) {
return null;
}
rest = rest.substring(1); // remove leading '/'
final String[] parts = rest.split("/", -1);
if (parts.length == 1) {
// /api/datacenters/{id}
if (parts[0].isEmpty()) return null;
return new Pair<>(parts[0], null);
}
if (parts.length == 2) {
// /api/datacenters/{id}/{subPath}
if (parts[0].isEmpty() || parts[1].isEmpty()) return null;
return new Pair<>(parts[0], parts[1]);
}
// deeper paths not handled here
return null;
}
}

View File

@ -20,6 +20,7 @@ package org.apache.cloudstack.veeam.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
public class ResponseMapper {
@ -36,6 +37,7 @@ public class ResponseMapper {
private static void configure(final ObjectMapper mapper) {
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// If you ever add enums etc:
// mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
// mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);

View File

@ -24,8 +24,11 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.api.dto.Fault;
import org.apache.cloudstack.veeam.api.response.FaultResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public final class ResponseWriter {
private static final Logger LOGGER = LogManager.getLogger(ResponseWriter.class);
private final ResponseMapper mapper;
@ -62,6 +65,8 @@ public final class ResponseWriter {
return;
}
LOGGER.info("Writing response: {}\n{}", status, payload);
resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
resp.setHeader("Content-Type", contentType);
resp.getWriter().write(payload);

View File

@ -33,6 +33,7 @@
<bean id="veeamControlApiService" class="org.apache.cloudstack.veeam.api.ApiService" />
<bean id="vmsRouteHandler" class="org.apache.cloudstack.veeam.api.VmsRouteHandler"/>
<bean id="dataCentersRouteHandler" class="org.apache.cloudstack.veeam.api.DataCentersRouteHandler"/>
<bean id="veeamControlSsoService" class="org.apache.cloudstack.veeam.sso.SsoService"/>
<bean id="veeamControlService" class="org.apache.cloudstack.veeam.VeeamControlServiceImpl" >

View File

@ -30,8 +30,11 @@ import com.cloud.utils.PropertiesUtil;
public class ServerPropertiesUtil {
private static final Logger logger = LoggerFactory.getLogger(ServerPropertiesUtil.class);
protected static final String PROPERTIES_FILE = "server.properties";
protected static final AtomicReference<Properties> propertiesRef = new AtomicReference<>();
protected static final String KEYSTORE_FILE = "https.keystore";
protected static final String KEYSTORE_PASSWORD = "https.keystore.password";
public static String getProperty(String name) {
Properties props = propertiesRef.get();
@ -55,4 +58,12 @@ public class ServerPropertiesUtil {
}
return tempProps.getProperty(name);
}
public static String getKeystoreFile() {
return getProperty(KEYSTORE_FILE);
}
public static String getKeystorePassword() {
return getProperty(KEYSTORE_PASSWORD);
}
}