blob: f641b7182b8f5c9b506073fa3e4bceebdd93961b [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
19import com.google.common.collect.ImmutableList;
20import com.google.common.collect.ImmutableSet;
21import com.google.common.io.ByteStreams;
22
23import java.io.File;
24import java.io.FileInputStream;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.PrintWriter;
29import java.text.SimpleDateFormat;
30import java.util.Date;
31import java.util.HashSet;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070032import java.util.Random;
33import java.util.Set;
34import java.util.Timer;
35import java.util.TimerTask;
36import java.util.concurrent.TimeUnit;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070037
38import static com.google.common.base.Preconditions.*;
39
40/**
41 * Warden for tracking use of shared test cells.
42 */
43class Warden {
44
45 private static final String CELL_NOT_NULL = "Cell name cannot be null";
46 private static final String USER_NOT_NULL = "User name cannot be null";
47 private static final String KEY_NOT_NULL = "User key cannot be null";
48 private static final String UTF_8 = "UTF-8";
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070049
Thomas Vachuska0d337002016-05-04 10:00:18 -070050 private static final long TIMEOUT = 10; // 10 seconds
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070051 private static final int MAX_MINUTES = 240; // 4 hours max
52 private static final int MINUTE = 60_000; // 1 minute
Thomas Vachuskaf9872a02016-05-05 13:56:25 -070053 private static final int DEFAULT_MINUTES = 60;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070054
55 private final File log = new File("warden.log");
56
57 private final File cells = new File("cells");
58 private final File supported = new File(cells, "supported");
59 private final File reserved = new File(cells, "reserved");
60
61 private final Random random = new Random();
62
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070063 /**
64 * Creates a new cell warden.
65 */
66 Warden() {
67 random.setSeed(System.currentTimeMillis());
Thomas Vachuska419c8bd2016-05-10 14:36:07 -070068 Timer timer = new Timer("cell-pruner", true);
Thomas Vachuska71ff3322016-05-03 15:33:05 -070069 timer.schedule(new Reposessor(), MINUTE / 4, MINUTE / 2);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070070 }
71
72 /**
73 * Returns list of names of supported cells.
74 *
75 * @return list of cell names
76 */
77 Set<String> getCells() {
78 String[] list = supported.list();
79 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
80 }
81
82 /**
83 * Returns list of names of available cells.
84 *
85 * @return list of cell names
86 */
Thomas Vachuska419c8bd2016-05-10 14:36:07 -070087 private Set<String> getAvailableCells() {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070088 Set<String> available = new HashSet<>(getCells());
89 available.removeAll(getReservedCells());
90 return ImmutableSet.copyOf(available);
91 }
92
93 /**
94 * Returns list of names of reserved cells.
95 *
96 * @return list of cell names
97 */
Thomas Vachuska419c8bd2016-05-10 14:36:07 -070098 private Set<String> getReservedCells() {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070099 String[] list = reserved.list();
100 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
101 }
102
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700103 /**
104 * Returns reservation for the specified user.
105 *
106 * @param userName user name
107 * @return cell reservation record or null if user does not have one
108 */
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700109 private Reservation currentUserReservation(String userName) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700110 checkNotNull(userName, USER_NOT_NULL);
111 for (String cellName : getReservedCells()) {
112 Reservation reservation = currentCellReservation(cellName);
113 if (reservation != null && userName.equals(reservation.userName)) {
114 return reservation;
115 }
116 }
117 return null;
118 }
119
120 /**
121 * Returns the name of the user who reserved the given cell.
122 *
123 * @param cellName cell name
124 * @return cell reservation record or null if cell is not reserved
125 */
126 Reservation currentCellReservation(String cellName) {
127 checkNotNull(cellName, CELL_NOT_NULL);
128 File cellFile = new File(reserved, cellName);
129 if (!cellFile.exists()) {
130 return null;
131 }
132 try (InputStream stream = new FileInputStream(cellFile)) {
133 return new Reservation(new String(ByteStreams.toByteArray(stream), "UTF-8"));
134 } catch (IOException e) {
135 throw new IllegalStateException("Unable to get current user for cell " + cellName, e);
136 }
137 }
138
139 /**
140 * Reserves a cell for the specified user and their public access key.
141 *
142 * @param userName user name
143 * @param sshKey user ssh public key
144 * @param minutes number of minutes for reservation
145 * @return reserved cell definition
146 */
147 synchronized String borrowCell(String userName, String sshKey, int minutes) {
148 checkNotNull(userName, USER_NOT_NULL);
149 checkNotNull(sshKey, KEY_NOT_NULL);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700150 checkArgument(minutes < MAX_MINUTES, "Number of minutes must be less than %d", MAX_MINUTES);
151 long now = System.currentTimeMillis();
152 Reservation reservation = currentUserReservation(userName);
153 if (reservation == null) {
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700154 checkArgument(minutes >= 0, "Number of minutes must be non-negative");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700155 Set<String> cells = getAvailableCells();
156 checkState(!cells.isEmpty(), "No cells are presently available");
157 String cellName = ImmutableList.copyOf(cells).get(random.nextInt(cells.size()));
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700158 reservation = new Reservation(cellName, userName, now, minutes == 0 ? DEFAULT_MINUTES : minutes);
Thomas Vachuska0d337002016-05-04 10:00:18 -0700159 } else if (minutes == 0) {
160 // If minutes are 0, simply return the cell definition
161 return getCellDefinition(reservation.cellName);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700162 } else {
163 reservation = new Reservation(reservation.cellName, userName, now, minutes);
164 }
165
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700166 reserveCell(reservation);
167 createCell(reservation, sshKey);
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700168 log(userName, reservation.cellName, "borrowed for " + reservation.duration + " minutes");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700169 return getCellDefinition(reservation.cellName);
170 }
171
172 /**
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700173 * Returns the specified cell for the specified user and their public access key.
174 *
175 * @param userName user name
176 */
177 synchronized void returnCell(String userName) {
178 checkNotNull(userName, USER_NOT_NULL);
179 Reservation reservation = currentUserReservation(userName);
180 checkState(reservation != null, "User %s has no cell reservations", userName);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700181
182 unreserveCell(reservation);
183 destroyCell(reservation);
184 log(userName, reservation.cellName, "returned");
185 }
186
187 /**
188 * Reserves the specified cell for the user the source file and writes the
189 * specified content to the target file.
190 *
191 * @param reservation cell reservation record
192 */
193 private void reserveCell(Reservation reservation) {
194 File cellFile = new File(reserved, reservation.cellName);
195 try (FileOutputStream stream = new FileOutputStream(cellFile)) {
196 stream.write(reservation.encode().getBytes(UTF_8));
197 } catch (IOException e) {
198 throw new IllegalStateException("Unable to reserve cell " + reservation.cellName, e);
199 }
200 }
201
Thomas Vachuska56179942016-05-10 21:15:49 -0700202 /**
203 * Returns the cell definition of the specified cell.
204 *
205 * @param cellName cell name
206 * @return cell definition
207 */
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700208 private String getCellDefinition(String cellName) {
Thomas Vachuska56179942016-05-10 21:15:49 -0700209 CellInfo cellInfo = getCellInfo(cellName);
210 return exec(String.format("ssh %s warden/bin/cell-def %s",
211 cellInfo.hostName, cellInfo.cellName));
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700212 }
213
214 /**
215 * Cancels the specified reservation.
216 *
217 * @param reservation reservation record
218 */
219 private void unreserveCell(Reservation reservation) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700220 checkState(new File(reserved, reservation.cellName).delete(),
221 "Unable to return cell %s", reservation.cellName);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700222 }
223
224 /**
225 * Creates the cell for the specified user SSH key.
226 *
227 * @param reservation cell reservation
228 * @param sshKey ssh key
229 */
230 private void createCell(Reservation reservation, String sshKey) {
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700231 CellInfo cellInfo = getCellInfo(reservation.cellName);
232 String cmd = String.format("ssh %s warden/bin/create-cell %s %s %s",
233 cellInfo.hostName, cellInfo.cellName,
234 cellInfo.ipPrefix, sshKey);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700235 exec(cmd);
236 }
237
238 /**
239 * Destroys the specified cell.
240 *
241 * @param reservation reservation record
242 */
243 private void destroyCell(Reservation reservation) {
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700244 CellInfo cellInfo = getCellInfo(reservation.cellName);
245 exec(String.format("ssh %s warden/bin/destroy-cell %s",
246 cellInfo.hostName, cellInfo.cellName));
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700247 }
248
249 /**
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700250 * Reads the information about the specified cell.
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700251 *
252 * @param cellName cell name
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700253 * @return cell information
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700254 */
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700255 private CellInfo getCellInfo(String cellName) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700256 File cellFile = new File(supported, cellName);
257 try (InputStream stream = new FileInputStream(cellFile)) {
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700258 String[] fields = new String(ByteStreams.toByteArray(stream), UTF_8).split(" ");
259 return new CellInfo(cellName, fields[0], fields[1]);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700260 } catch (IOException e) {
261 throw new IllegalStateException("Unable to definition for cell " + cellName, e);
262 }
263 }
264
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700265 // Executes the specified command.
266 private String exec(String command) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700267 try {
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700268 Process process = Runtime.getRuntime().exec(command);
269 String output = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700270 process.waitFor(TIMEOUT, TimeUnit.SECONDS);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700271 return process.exitValue() == 0 ? output : null;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700272 } catch (Exception e) {
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700273 throw new IllegalStateException("Unable to execute " + command);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700274 }
275 }
276
277 // Creates an audit log entry.
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700278 private void log(String userName, String cellName, String action) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700279 try (FileOutputStream fos = new FileOutputStream(log, true);
280 PrintWriter pw = new PrintWriter(fos)) {
281 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
282 pw.println(String.format("%s\t%s\t%s\t%s", format.format(new Date()),
283 userName, cellName, action));
284 pw.flush();
285 } catch (IOException e) {
286 throw new IllegalStateException("Unable to log reservation action", e);
287 }
288 }
289
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700290 // Carrier of cell information
291 private final class CellInfo {
292 final String cellName;
293 final String hostName;
294 final String ipPrefix;
295
296 private CellInfo(String cellName, String hostName, String ipPrefix) {
297 this.cellName = cellName;
298 this.hostName = hostName;
299 this.ipPrefix = ipPrefix;
300 }
301 }
302
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700303 // Task for re-possessing overdue cells
Thomas Vachuska419c8bd2016-05-10 14:36:07 -0700304 private final class Reposessor extends TimerTask {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700305 @Override
306 public void run() {
307 long now = System.currentTimeMillis();
308 for (String cellName : getReservedCells()) {
309 Reservation reservation = currentCellReservation(cellName);
310 if (reservation != null &&
311 (reservation.time + reservation.duration * MINUTE) < now) {
312 try {
313 returnCell(reservation.userName);
314 } catch (Exception e) {
315 e.printStackTrace();
316 }
317 }
318 }
319 }
320 }
321}