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