1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.kernel.bundleinfo;
23
24 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
25 import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
26 import static org.opencastproject.util.EqualsUtil.ne;
27 import static org.opencastproject.util.Jsons.arr;
28 import static org.opencastproject.util.Jsons.obj;
29 import static org.opencastproject.util.Jsons.p;
30 import static org.opencastproject.util.RestUtil.R.notFound;
31 import static org.opencastproject.util.RestUtil.R.ok;
32 import static org.opencastproject.util.data.Collections.set;
33 import static org.opencastproject.util.data.Collections.toArray;
34
35 import org.opencastproject.util.Jsons;
36 import org.opencastproject.util.doc.rest.RestParameter;
37 import org.opencastproject.util.doc.rest.RestQuery;
38 import org.opencastproject.util.doc.rest.RestResponse;
39 import org.opencastproject.util.doc.rest.RestService;
40
41 import org.osgi.service.component.ComponentContext;
42 import org.osgi.service.component.annotations.Activate;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import java.util.List;
47 import java.util.Optional;
48 import java.util.Set;
49 import java.util.function.Function;
50 import java.util.stream.Collectors;
51 import java.util.stream.StreamSupport;
52
53 import javax.servlet.http.HttpServletResponse;
54 import javax.ws.rs.DELETE;
55 import javax.ws.rs.DefaultValue;
56 import javax.ws.rs.GET;
57 import javax.ws.rs.Path;
58 import javax.ws.rs.Produces;
59 import javax.ws.rs.QueryParam;
60 import javax.ws.rs.core.Response;
61
62
63 @RestService(
64 name = "systemInfo",
65 title = "System Bundle Info",
66 notes = { "This is used to display the version information on the login page." },
67 abstractText = "The system bundle info endpoint yields information about the running OSGi bundles of Opencast.")
68 public abstract class BundleInfoRestEndpoint {
69
70 private static final Logger logger = LoggerFactory.getLogger(BundleInfoRestEndpoint.class);
71
72 private static final String DEFAULT_BUNDLE_PREFIX = "opencast";
73
74 protected abstract BundleInfoDb getDb();
75
76 private long lastModified = 0;
77
78 @Activate
79 public void activate(ComponentContext cc) {
80 lastModified = cc.getBundleContext().getBundle().getLastModified();
81 }
82
83 @GET
84
85
86 @Path("bundles/list")
87 @Produces(APPLICATION_JSON)
88 @RestQuery(
89 name = "list",
90 description = "Return a list of all running bundles on the whole cluster.",
91 responses = {
92 @RestResponse(description = "A list of bundles.", responseCode = HttpServletResponse.SC_OK) },
93 returnDescription = "The search results, expressed as xml or json.")
94 public Response getVersions() {
95 List<Jsons.Val> bundleInfos = getDb().getBundles().stream()
96 .map(b -> (Jsons.Val) bundleInfo(b))
97 .toList();
98
99 return ok(obj(p("bundleInfos", arr(bundleInfos)), p("count", bundleInfos.size())));
100 }
101
102
103 @GET
104 @Path("bundles/check")
105 @RestQuery(
106 name = "check",
107 description = "Check if all bundles throughout the cluster have the same OSGi bundle version and build number.",
108 restParameters = {
109 @RestParameter(
110 name = "prefix",
111 description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
112 isRequired = false,
113 defaultValue = DEFAULT_BUNDLE_PREFIX,
114 type = RestParameter.Type.STRING) },
115 responses = {
116 @RestResponse(description = "true/false", responseCode = HttpServletResponse.SC_OK),
117 @RestResponse(description = "cannot find any bundles with the given prefix",
118 responseCode = HttpServletResponse.SC_NOT_FOUND) },
119 returnDescription = "The search results, expressed as xml or json.")
120 public Response checkBundles(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
121 return withBundles(prefixes, infos -> {
122 final String bundleVersion = infos.get(0).getBundleVersion();
123 final Optional<String> buildNumber = infos.get(0).getBuildNumber();
124 for (BundleInfo a : infos) {
125 if (ne(a.getBundleVersion(), bundleVersion) || ne(a.getBuildNumber(), buildNumber)) {
126 return ok(TEXT_PLAIN_TYPE, "false");
127 }
128 }
129 return ok(TEXT_PLAIN_TYPE, "true");
130 });
131 }
132
133
134 @GET
135 @Path("bundles/version")
136 @Produces(APPLICATION_JSON)
137 @RestQuery(
138 name = "bundleVersion",
139 description = "Return the common OSGi build version and build number of all bundles matching the given prefix.",
140 restParameters = {
141 @RestParameter(
142 name = "prefix",
143 description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
144 isRequired = false,
145 defaultValue = DEFAULT_BUNDLE_PREFIX,
146 type = RestParameter.Type.STRING) },
147 responses = {
148 @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_OK),
149 @RestResponse(description = "No bundles with the given prefix",
150 responseCode = HttpServletResponse.SC_NOT_FOUND) },
151 returnDescription = "The search results as json.")
152 public Response getBundleVersion(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
153 return withBundles(prefixes, infos -> {
154 final Set<BundleVersion> versions = set();
155 for (BundleInfo bundle : infos) {
156 versions.add(bundle.getVersion());
157 }
158 final BundleInfo example = infos.get(0);
159 switch (versions.size()) {
160 case 0:
161
162 throw new Error("bug");
163 case 1:
164
165 return ok(obj(p("consistent", true))
166 .append(fullVersionJson(example.getVersion()))
167 .append(obj(p("last-modified", lastModified))));
168 default:
169
170 return ok(obj(
171 p("consistent", false),
172 p("versions",
173 arr(StreamSupport.stream(versions.spliterator(), false)
174 .map(v -> (Jsons.Val) fullVersionJson(v))
175 .collect(Collectors.toList())))
176 ));
177 }
178 });
179 }
180
181 @DELETE
182 @Path("bundles/host")
183 @RestQuery(
184 name = "clearHost",
185 description = "Removes the tracked bundles for a host. This is done automatically when you shut down "
186 + "Opencast. But this endpoint can be used to force this in case e.g. a machine got dropped. Make sure the "
187 + "host is actually gone! The database will be automatically rebuilt when Opencast on that host is "
188 + "(re)started.",
189 restParameters = {
190 @RestParameter(
191 name = "host",
192 description = "The name of the host to clear",
193 isRequired = true,
194 type = RestParameter.Type.STRING,
195 defaultValue = "") },
196 responses = {
197 @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_NO_CONTENT) },
198 returnDescription = "No data is returned.")
199 public Response clearHost(@QueryParam("host") String host) {
200 logger.debug("Removing tracked bundles of host: {}", host);
201 getDb().clear(host);
202 return Response.noContent().build();
203 }
204
205 public static final Jsons.Obj fullVersionJson(BundleVersion version) {
206 return obj(
207 p("version", version.getBundleVersion()),
208 p("buildNumber", version.getBuildNumber().map(Jsons::stringVal)));
209 }
210
211 public static Jsons.Obj bundleInfoJson(BundleInfo bundle) {
212 return obj(p("host", bundle.getHost()), p("bundleSymbolicName", bundle.getBundleSymbolicName()),
213 p("bundleId", bundle.getBundleId())).append(fullVersionJson(bundle.getVersion()));
214 }
215
216 public static final Jsons.Obj bundleInfo(BundleInfo bundle) {
217 return bundleInfoJson(bundle);
218 }
219
220
221 private Response withBundles(List<String> prefixes, Function<List<BundleInfo>, Response> f) {
222 final List<BundleInfo> info = getDb().getBundles(toArray(String.class, prefixes));
223 if (info.size() > 0) {
224 return f.apply(info);
225 } else {
226 return notFound("No bundles match one of the given prefixes");
227 }
228 }
229 }