diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/RouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/RouteHandler.java index 25c4dfbf8f6..5e0db99d161 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/RouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/RouteHandler.java @@ -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; + } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServer.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServer.java index aa03cddd2f7..539e89e8473 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServer.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServer.java @@ -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 headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + final String name = headerNames.nextElement(); + final Enumeration values = request.getHeaders(name); + while (values.hasMoreElements()) { + sb.append(name).append("=").append(values.nextElement()).append("; "); + } + } + return sb.toString(); + } } \ No newline at end of file diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlService.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlService.java index 87233a3ebd8..adf02be8dd1 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlService.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlService.java @@ -30,7 +30,7 @@ public interface VeeamControlService extends PluggableService, Configurable { ConfigKey Port = new ConfigKey<>("Advanced", Integer.class, "integration.veeam.control.port", "8090", "Port for Veeam Integration REST API server", false); ConfigKey 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 Username = new ConfigKey<>("Advanced", String.class, "integration.veeam.api.username", "veeam", "Username for Basic Auth on Veeam Integration REST API server", true); ConfigKey Password = new ConfigKey<>("Advanced", String.class, "integration.veeam.api.password", diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java index bf064e27f02..7c38e4cf249 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java @@ -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 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)) { diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ApiService.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ApiService.java index d2a9cf386f0..bb37c300d84 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ApiService.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/ApiService.java @@ -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 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 links, String href, String rel) { + links.add(new Link(href, rel)); + } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java new file mode 100644 index 00000000000..d297fe9b516 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandler.java @@ -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 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 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 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 listStoragePoolsByDcId(final long dcId) { + return storagePoolJoinDao.listAll(); + } + + protected List 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 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); + } +} \ No newline at end of file diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java index 166794e37bb..23f626e326e 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/VmsRouteHandler.java @@ -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); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/DataCenterVOToDataCenterConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/DataCenterVOToDataCenterConverter.java new file mode 100644 index 00000000000..395bb233ea5 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/DataCenterVOToDataCenterConverter.java @@ -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 toDCList(final List srcList) { + return srcList.stream() + .map(DataCenterVOToDataCenterConverter::toDataCenter) + .collect(Collectors.toList()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/StoreVOToStorageDomainConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/StoreVOToStorageDomainConverter.java new file mode 100644 index 00000000000..071ebc92c14 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/StoreVOToStorageDomainConverter.java @@ -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 don’t 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 toStorageDomainListFromPools(final List 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 don’t have these values readily; keep as "0" or omit (null) + sd.committed = "0"; + sd.available = null; // oVirt’s 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 didn’t) + 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 toStorageDomainListFromStores(final List 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 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 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; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java index ba5c831169d..760ab5c758c 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverter.java @@ -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; } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Api.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Api.java new file mode 100644 index 00000000000..2571f32111f --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Api.java @@ -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 { + + // repeated + @JacksonXmlElementWrapper(useWrapping = false) + public List link; + + // (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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/DataCenter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/DataCenter.java new file mode 100644 index 00000000000..acba378032c --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/DataCenter.java @@ -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; + + public String href; + public String id; + + public DataCenter() {} +} \ No newline at end of file diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/DataCenters.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/DataCenters.java new file mode 100644 index 00000000000..24e6f288425 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/DataCenters.java @@ -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; + + public DataCenters() {} + public DataCenters(final List dataCenter) { + this.dataCenter = dataCenter; + } +} + diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/EmptyElement.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/EmptyElement.java new file mode 100644 index 00000000000..54d65d8529b --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/EmptyElement.java @@ -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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/EmptyElementSerializer.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/EmptyElementSerializer.java new file mode 100644 index 00000000000..4b6a407aecf --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/EmptyElementSerializer.java @@ -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: . + */ +public final class EmptyElementSerializer extends JsonSerializer { + @Override + public void serialize(EmptyElement value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeEndObject(); + } +} + diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ProductInfo.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ProductInfo.java new file mode 100644 index 00000000000..e3618b0e6f9 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ProductInfo.java @@ -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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SpecialObjectRef.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SpecialObjectRef.java new file mode 100644 index 00000000000..39b52c8bd0d --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SpecialObjectRef.java @@ -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; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SpecialObjects.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SpecialObjects.java new file mode 100644 index 00000000000..dc747fa177e --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SpecialObjects.java @@ -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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Storage.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Storage.java new file mode 100644 index 00000000000..edf411ec9be --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Storage.java @@ -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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/StorageDomain.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/StorageDomain.java new file mode 100644 index 00000000000..0b4663fd039 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/StorageDomain.java @@ -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; + + public StorageDomain() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/StorageDomains.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/StorageDomains.java new file mode 100644 index 00000000000..7fffa8f9a8f --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/StorageDomains.java @@ -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; + + public StorageDomains() {} + public StorageDomains(List storageDomain) { + this.storageDomain = storageDomain; + } +} \ No newline at end of file diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Summary.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Summary.java new file mode 100644 index 00000000000..992590f5f97 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Summary.java @@ -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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SummaryCount.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SummaryCount.java new file mode 100644 index 00000000000..a0266a2b89a --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SummaryCount.java @@ -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; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SupportedVersions.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SupportedVersions.java new file mode 100644 index 00000000000..7c73b9e5d94 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/SupportedVersions.java @@ -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; + + public SupportedVersions() {} + public SupportedVersions(final List version) { + this.version = version; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java new file mode 100644 index 00000000000..cd4601838d1 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Version.java @@ -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() {} +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Vm.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Vm.java index ce1b64e5303..4bba580a971 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Vm.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/Vm.java @@ -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; // related resources diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/BearerOrBasicAuthFilter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/BearerOrBasicAuthFilter.java new file mode 100644 index 00000000000..5a83299207d --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/BearerOrBasicAuthFilter.java @@ -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 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; it’s 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: don’t 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("ErrorUnauthorized"); + } + 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)); + } + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/sso/SsoService.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/sso/SsoService.java index 3aed77efa20..fcd984ffce0 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/sso/SsoService.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/sso/SsoService.java @@ -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 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("\"", "\\\""); + } } } diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java new file mode 100644 index 00000000000..11a5f2b337d --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java @@ -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 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; + } +} diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java index a56dde4c75e..46b3a993aa7 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseMapper.java @@ -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); diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java index a40ebc860a3..7dcdc3e647f 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java @@ -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); diff --git a/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml b/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml index 0d1d3573031..6e75d838438 100644 --- a/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml +++ b/plugins/integrations/veeam-control-service/src/main/resources/META-INF/cloudstack/veeam-control-service/spring-veeam-control-service-context.xml @@ -33,6 +33,7 @@ + diff --git a/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java b/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java index 14d24dbb641..52642cf0370 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/server/ServerPropertiesUtil.java @@ -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 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); + } }