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.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  /** Bundle information via REST. */
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    // path prefix "bundles" is contained here and not in the path annotation of the class
94    // See https://opencast.jira.com/browse/MH-9768
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   /** Return true if all bundles have the same bundle version and build number. */
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   /** Return the common version of all bundles matching the given prefix. */
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           // no versions...
171           throw new Error("bug");
172         case 1:
173           // all versions align
174           return ok(obj(p("consistent", true))
175               .append(fullVersionJson(example.getVersion()))
176               .append(obj(p("last-modified", lastModified))));
177         default:
178           // multiple versions found
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   /** Run <code>f</code> if there is at least one bundle matching the given prefixes. */
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 }