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", 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   /** Return the common version of all bundles matching the given prefix. */
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           // no versions...
159           throw new Error("bug");
160         case 1:
161           // all versions align
162           return ok(obj(p("consistent", true))
163               .append(fullVersionJson(example.getVersion()))
164               .append(obj(p("last-modified", lastModified))));
165         default:
166           // multiple versions found
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   /** Run <code>f</code> if there is at least one bundle matching the given prefixes. */
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 }