View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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  /** Bundle information via REST. */
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    // path prefix "bundles" is contained here and not in the path annotation of the class
85    // See https://opencast.jira.com/browse/MH-9768
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   /** Return true if all bundles have the same bundle version and build number. */
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   /** Return the common version of all bundles matching the given prefix. */
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           // no versions...
162           throw new Error("bug");
163         case 1:
164           // all versions align
165           return ok(obj(p("consistent", true))
166               .append(fullVersionJson(example.getVersion()))
167               .append(obj(p("last-modified", lastModified))));
168         default:
169           // multiple versions found
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   /** Run <code>f</code> if there is at least one bundle matching the given prefixes. */
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 }