blob: 8ce603794150407a26dc9c2ba0a1f59ee14218f2 [file] [log] [blame]
Thomas Vachuska1eff3a62016-05-03 01:07:24 -07001/*
2 * Copyright 2016 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onlab.warden;
18
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070019import com.google.common.collect.ImmutableSet;
Thomas Vachuska5420ba32016-05-13 14:45:25 -040020import com.google.common.collect.Lists;
21import com.google.common.collect.Maps;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070022import com.google.common.io.ByteStreams;
23
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileOutputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.io.PrintWriter;
30import java.text.SimpleDateFormat;
Thomas Vachuska5420ba32016-05-13 14:45:25 -040031import java.util.ArrayList;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070032import java.util.Date;
33import java.util.HashSet;
Thomas Vachuska5420ba32016-05-13 14:45:25 -040034import java.util.List;
35import java.util.Map;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070036import java.util.Random;
37import java.util.Set;
38import java.util.Timer;
39import java.util.TimerTask;
40import java.util.concurrent.TimeUnit;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070041
42import static com.google.common.base.Preconditions.*;
43
44/**
45 * Warden for tracking use of shared test cells.
46 */
47class Warden {
48
49 private static final String CELL_NOT_NULL = "Cell name cannot be null";
50 private static final String USER_NOT_NULL = "User name cannot be null";
51 private static final String KEY_NOT_NULL = "User key cannot be null";
52 private static final String UTF_8 = "UTF-8";
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070053
Thomas Vachuska0d337002016-05-04 10:00:18 -070054 private static final long TIMEOUT = 10; // 10 seconds
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070055 private static final int MAX_MINUTES = 240; // 4 hours max
56 private static final int MINUTE = 60_000; // 1 minute
Thomas Vachuskaf9872a02016-05-05 13:56:25 -070057 private static final int DEFAULT_MINUTES = 60;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070058
Thomas Vachuska5420ba32016-05-13 14:45:25 -040059 private static final String DEFAULT_SPEC = "3+1";
60
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070061 private final File log = new File("warden.log");
62
Thomas Vachuska5420ba32016-05-13 14:45:25 -040063 // Allow overriding these for unit tests.
64 static String cmdPrefix = "";
65 static File root = new File(".");
66
67 private final File cells = new File(root, "cells");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070068 private final File supported = new File(cells, "supported");
69 private final File reserved = new File(cells, "reserved");
70
71 private final Random random = new Random();
72
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070073 /**
74 * Creates a new cell warden.
75 */
76 Warden() {
Thomas Vachuska5420ba32016-05-13 14:45:25 -040077 reserved.mkdirs();
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070078 random.setSeed(System.currentTimeMillis());
Thomas Vachuska419c8bd2016-05-10 14:36:07 -070079 Timer timer = new Timer("cell-pruner", true);
Thomas Vachuska71ff3322016-05-03 15:33:05 -070080 timer.schedule(new Reposessor(), MINUTE / 4, MINUTE / 2);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070081 }
82
83 /**
84 * Returns list of names of supported cells.
85 *
86 * @return list of cell names
87 */
88 Set<String> getCells() {
89 String[] list = supported.list();
90 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
91 }
92
93 /**
94 * Returns list of names of available cells.
95 *
96 * @return list of cell names
97 */
Thomas Vachuska5420ba32016-05-13 14:45:25 -040098 Set<String> getAvailableCells() {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070099 Set<String> available = new HashSet<>(getCells());
100 available.removeAll(getReservedCells());
101 return ImmutableSet.copyOf(available);
102 }
103
104 /**
105 * Returns list of names of reserved cells.
106 *
107 * @return list of cell names
108 */
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400109 Set<String> getReservedCells() {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700110 String[] list = reserved.list();
111 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
112 }
113
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700114 /**
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400115 * Returns the host name on which the specified cell is hosted.
116 *
117 * @param cellName cell name
118 * @return host name where the cell runs
119 */
120 String getCellHost(String cellName) {
121 return getCellInfo(cellName).hostName;
122 }
123
124 /**
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700125 * Returns reservation for the specified user.
126 *
127 * @param userName user name
128 * @return cell reservation record or null if user does not have one
129 */
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400130 Reservation currentUserReservation(String userName) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700131 checkNotNull(userName, USER_NOT_NULL);
132 for (String cellName : getReservedCells()) {
133 Reservation reservation = currentCellReservation(cellName);
134 if (reservation != null && userName.equals(reservation.userName)) {
135 return reservation;
136 }
137 }
138 return null;
139 }
140
141 /**
142 * Returns the name of the user who reserved the given cell.
143 *
144 * @param cellName cell name
145 * @return cell reservation record or null if cell is not reserved
146 */
147 Reservation currentCellReservation(String cellName) {
148 checkNotNull(cellName, CELL_NOT_NULL);
149 File cellFile = new File(reserved, cellName);
150 if (!cellFile.exists()) {
151 return null;
152 }
153 try (InputStream stream = new FileInputStream(cellFile)) {
154 return new Reservation(new String(ByteStreams.toByteArray(stream), "UTF-8"));
155 } catch (IOException e) {
156 throw new IllegalStateException("Unable to get current user for cell " + cellName, e);
157 }
158 }
159
160 /**
161 * Reserves a cell for the specified user and their public access key.
162 *
163 * @param userName user name
164 * @param sshKey user ssh public key
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400165 * @param minutes optional number of minutes for reservation
166 * @param cellSpec optional cell specification string
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700167 * @return reserved cell definition
168 */
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400169 synchronized String borrowCell(String userName, String sshKey, int minutes,
170 String cellSpec) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700171 checkNotNull(userName, USER_NOT_NULL);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400172 checkArgument(userName.matches("[\\w]+"), "Invalid user name %s", userName);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700173 checkNotNull(sshKey, KEY_NOT_NULL);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700174 checkArgument(minutes < MAX_MINUTES, "Number of minutes must be less than %d", MAX_MINUTES);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400175 checkArgument(minutes >= 0, "Number of minutes must be non-negative");
176 checkArgument(cellSpec == null || cellSpec.matches("[\\d]+\\+[0-1]"),
177 "Invalid cell spec string %s", cellSpec);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700178 Reservation reservation = currentUserReservation(userName);
179 if (reservation == null) {
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400180 // If there is no reservation for the user, create one
181 String cellName = findAvailableCell();
182 reservation = new Reservation(cellName, userName, System.currentTimeMillis(),
183 minutes == 0 ? DEFAULT_MINUTES : minutes,
184 cellSpec == null ? DEFAULT_SPEC : cellSpec);
Thomas Vachuska0d337002016-05-04 10:00:18 -0700185 } else if (minutes == 0) {
186 // If minutes are 0, simply return the cell definition
187 return getCellDefinition(reservation.cellName);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700188 } else {
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400189 // If minutes are > 0, update the existing cell reservation
190 reservation = new Reservation(reservation.cellName, userName,
191 System.currentTimeMillis(), minutes,
192 reservation.cellSpec);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700193 }
194
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700195 reserveCell(reservation);
196 createCell(reservation, sshKey);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400197 log(userName, reservation.cellName, reservation.cellSpec,
198 "borrowed for " + reservation.duration + " minutes");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700199 return getCellDefinition(reservation.cellName);
200 }
201
202 /**
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400203 * Returns name of an available cell. Cell is chosen based on the load
204 * of its hosting server; a random one will be chosen from the set of
205 * cells hosted by the least loaded server.
206 *
207 * @return name of an available cell
208 */
209 private String findAvailableCell() {
210 Set<String> cells = getAvailableCells();
211 checkState(!cells.isEmpty(), "No cells are presently available");
212 Map<String, ServerInfo> load = Maps.newHashMap();
213
214 cells.stream().map(this::getCellInfo)
215 .forEach(info -> load.compute(info.hostName, (k, v) -> v == null ?
216 new ServerInfo(info.hostName) : v.bumpLoad(info)));
217
218 List<ServerInfo> servers = new ArrayList<>(load.values());
Thomas Vachuskaea316092016-05-16 09:50:44 -0700219 servers.sort((a, b) -> b.load - a.load);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400220 ServerInfo server = servers.get(0);
221 return server.cells.get(random.nextInt(server.cells.size())).cellName;
222 }
223
224 /**
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700225 * Returns the specified cell for the specified user and their public access key.
226 *
227 * @param userName user name
228 */
229 synchronized void returnCell(String userName) {
230 checkNotNull(userName, USER_NOT_NULL);
231 Reservation reservation = currentUserReservation(userName);
232 checkState(reservation != null, "User %s has no cell reservations", userName);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700233
234 unreserveCell(reservation);
235 destroyCell(reservation);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400236 log(userName, reservation.cellName, reservation.cellSpec, "returned");
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700237 }
238
239 /**
240 * Reserves the specified cell for the user the source file and writes the
241 * specified content to the target file.
242 *
243 * @param reservation cell reservation record
244 */
245 private void reserveCell(Reservation reservation) {
246 File cellFile = new File(reserved, reservation.cellName);
247 try (FileOutputStream stream = new FileOutputStream(cellFile)) {
248 stream.write(reservation.encode().getBytes(UTF_8));
249 } catch (IOException e) {
250 throw new IllegalStateException("Unable to reserve cell " + reservation.cellName, e);
251 }
252 }
253
Thomas Vachuska56179942016-05-10 21:15:49 -0700254 /**
255 * Returns the cell definition of the specified cell.
256 *
257 * @param cellName cell name
258 * @return cell definition
259 */
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700260 private String getCellDefinition(String cellName) {
Thomas Vachuska56179942016-05-10 21:15:49 -0700261 CellInfo cellInfo = getCellInfo(cellName);
262 return exec(String.format("ssh %s warden/bin/cell-def %s",
263 cellInfo.hostName, cellInfo.cellName));
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700264 }
265
266 /**
267 * Cancels the specified reservation.
268 *
269 * @param reservation reservation record
270 */
271 private void unreserveCell(Reservation reservation) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700272 checkState(new File(reserved, reservation.cellName).delete(),
273 "Unable to return cell %s", reservation.cellName);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700274 }
275
276 /**
277 * Creates the cell for the specified user SSH key.
278 *
279 * @param reservation cell reservation
280 * @param sshKey ssh key
281 */
282 private void createCell(Reservation reservation, String sshKey) {
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700283 CellInfo cellInfo = getCellInfo(reservation.cellName);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400284 String cmd = String.format("ssh %s warden/bin/create-cell %s %s %s %s",
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700285 cellInfo.hostName, cellInfo.cellName,
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400286 cellInfo.ipPrefix, reservation.cellSpec, sshKey);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700287 exec(cmd);
288 }
289
290 /**
291 * Destroys the specified cell.
292 *
293 * @param reservation reservation record
294 */
295 private void destroyCell(Reservation reservation) {
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700296 CellInfo cellInfo = getCellInfo(reservation.cellName);
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400297 exec(String.format("ssh %s warden/bin/destroy-cell %s %s",
298 cellInfo.hostName, cellInfo.cellName, reservation.cellSpec));
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700299 }
300
301 /**
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700302 * Reads the information about the specified cell.
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700303 *
304 * @param cellName cell name
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700305 * @return cell information
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700306 */
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700307 private CellInfo getCellInfo(String cellName) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700308 File cellFile = new File(supported, cellName);
309 try (InputStream stream = new FileInputStream(cellFile)) {
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700310 String[] fields = new String(ByteStreams.toByteArray(stream), UTF_8).split(" ");
311 return new CellInfo(cellName, fields[0], fields[1]);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700312 } catch (IOException e) {
313 throw new IllegalStateException("Unable to definition for cell " + cellName, e);
314 }
315 }
316
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700317 // Executes the specified command.
318 private String exec(String command) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700319 try {
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400320 Process process = Runtime.getRuntime().exec(cmdPrefix + command);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700321 String output = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700322 process.waitFor(TIMEOUT, TimeUnit.SECONDS);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700323 return process.exitValue() == 0 ? output : null;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700324 } catch (Exception e) {
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700325 throw new IllegalStateException("Unable to execute " + command);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700326 }
327 }
328
329 // Creates an audit log entry.
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400330 private void log(String userName, String cellName, String cellSpec, String action) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700331 try (FileOutputStream fos = new FileOutputStream(log, true);
332 PrintWriter pw = new PrintWriter(fos)) {
333 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400334 pw.println(String.format("%s\t%s\t%s-%s\t%s", format.format(new Date()),
335 userName, cellName, cellSpec, action));
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700336 pw.flush();
337 } catch (IOException e) {
338 throw new IllegalStateException("Unable to log reservation action", e);
339 }
340 }
341
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700342 // Carrier of cell information
343 private final class CellInfo {
344 final String cellName;
345 final String hostName;
346 final String ipPrefix;
347
348 private CellInfo(String cellName, String hostName, String ipPrefix) {
349 this.cellName = cellName;
350 this.hostName = hostName;
351 this.ipPrefix = ipPrefix;
352 }
353 }
354
Thomas Vachuska5420ba32016-05-13 14:45:25 -0400355 // Carrier of cell server information
356 private final class ServerInfo {
357 final String hostName;
358 int load = 0;
359 List<CellInfo> cells = Lists.newArrayList();
360
361 private ServerInfo(String hostName) {
362 this.hostName = hostName;
363 }
364
365 private ServerInfo bumpLoad(CellInfo info) {
366 cells.add(info);
367 load++; // TODO: bump by cell size later
368 return this;
369 }
370 }
371
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700372 // Task for re-possessing overdue cells
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700373 private final class Reposessor extends TimerTask {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700374 @Override
375 public void run() {
376 long now = System.currentTimeMillis();
377 for (String cellName : getReservedCells()) {
378 Reservation reservation = currentCellReservation(cellName);
379 if (reservation != null &&
380 (reservation.time + reservation.duration * MINUTE) < now) {
381 try {
382 returnCell(reservation.userName);
383 } catch (Exception e) {
384 e.printStackTrace();
385 }
386 }
387 }
388 }
389 }
390}