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", responseCode = HttpServletResponse.SC_NOT_FOUND) },
118 returnDescription = "The search results, expressed as xml or json.")
119 public Response checkBundles(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
120 return withBundles(prefixes, infos -> {
121 final String bundleVersion = infos.get(0).getBundleVersion();
122 final Optional<String> buildNumber = infos.get(0).getBuildNumber();
123 for (BundleInfo a : infos) {
124 if (ne(a.getBundleVersion(), bundleVersion) || ne(a.getBuildNumber(), buildNumber))
125 return ok(TEXT_PLAIN_TYPE, "false");
126 }
127 return ok(TEXT_PLAIN_TYPE, "true");
128 });
129 }
130
131
132 @GET
133 @Path("bundles/version")
134 @Produces(APPLICATION_JSON)
135 @RestQuery(
136 name = "bundleVersion",
137 description = "Return the common OSGi build version and build number of all bundles matching the given prefix.",
138 restParameters = {
139 @RestParameter(
140 name = "prefix",
141 description = "The bundle name prefixes to check. Defaults to '" + DEFAULT_BUNDLE_PREFIX + "'.",
142 isRequired = false,
143 defaultValue = DEFAULT_BUNDLE_PREFIX,
144 type = RestParameter.Type.STRING) },
145 responses = {
146 @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_OK),
147 @RestResponse(description = "No bundles with the given prefix", responseCode = HttpServletResponse.SC_NOT_FOUND) },
148 returnDescription = "The search results as json.")
149 public Response getBundleVersion(@DefaultValue(DEFAULT_BUNDLE_PREFIX) @QueryParam("prefix") List<String> prefixes) {
150 return withBundles(prefixes, infos -> {
151 final Set<BundleVersion> versions = set();
152 for (BundleInfo bundle : infos) {
153 versions.add(bundle.getVersion());
154 }
155 final BundleInfo example = infos.get(0);
156 switch (versions.size()) {
157 case 0:
158
159 throw new Error("bug");
160 case 1:
161
162 return ok(obj(p("consistent", true))
163 .append(fullVersionJson(example.getVersion()))
164 .append(obj(p("last-modified", lastModified))));
165 default:
166
167 return ok(obj(
168 p("consistent", false),
169 p("versions",
170 arr(StreamSupport.stream(versions.spliterator(), false)
171 .map(v -> (Jsons.Val) fullVersionJson(v))
172 .collect(Collectors.toList())))
173 ));
174 }
175 });
176 }
177
178 @DELETE
179 @Path("bundles/host")
180 @RestQuery(
181 name = "clearHost",
182 description = "Removes the tracked bundles for a host. This is done automatically when you shut down "
183 + "Opencast. But this endpoint can be used to force this in case e.g. a machine got dropped. Make sure the "
184 + "host is actually gone! The database will be automatically rebuilt when Opencast on that host is "
185 + "(re)started.",
186 restParameters = {
187 @RestParameter(
188 name = "host",
189 description = "The name of the host to clear",
190 isRequired = true,
191 type = RestParameter.Type.STRING,
192 defaultValue = "") },
193 responses = {
194 @RestResponse(description = "Version structure", responseCode = HttpServletResponse.SC_NO_CONTENT) },
195 returnDescription = "No data is returned.")
196 public Response clearHost(@QueryParam("host") String host) {
197 logger.debug("Removing tracked bundles of host: {}", host);
198 getDb().clear(host);
199 return Response.noContent().build();
200 }
201
202 public static final Jsons.Obj fullVersionJson(BundleVersion version) {
203 return obj(p("version", version.getBundleVersion()), p("buildNumber", version.getBuildNumber().map(Jsons::stringVal)));
204 }
205
206 public static Jsons.Obj bundleInfoJson(BundleInfo bundle) {
207 return obj(p("host", bundle.getHost()), p("bundleSymbolicName", bundle.getBundleSymbolicName()),
208 p("bundleId", bundle.getBundleId())).append(fullVersionJson(bundle.getVersion()));
209 }
210
211 public static final Jsons.Obj bundleInfo(BundleInfo bundle) {
212 return bundleInfoJson(bundle);
213 }
214
215
216 private Response withBundles(List<String> prefixes, Function<List<BundleInfo>, Response> f) {
217 final List<BundleInfo> info = getDb().getBundles(toArray(String.class, prefixes));
218 if (info.size() > 0) {
219 return f.apply(info);
220 } else {
221 return notFound("No bundles match one of the given prefixes");
222 }
223 }
224 }