blob: f6c091a32109daeb89c5bb30745163c6e6414664 [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
50 private static final String AUTHORIZED_KEYS = "authorized_keys";
51
Thomas Vachuska0d337002016-05-04 10:00:18 -070052 private static final long TIMEOUT = 10; // 10 seconds
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070053 private static final int MAX_MINUTES = 240; // 4 hours max
54 private static final int MINUTE = 60_000; // 1 minute
Thomas Vachuskaf9872a02016-05-05 13:56:25 -070055 private static final int DEFAULT_MINUTES = 60;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070056
57 private final File log = new File("warden.log");
58
59 private final File cells = new File("cells");
60 private final File supported = new File(cells, "supported");
61 private final File reserved = new File(cells, "reserved");
62
63 private final Random random = new Random();
64
65 private final Timer timer = new Timer("cell-pruner", true);
66
67 /**
68 * Creates a new cell warden.
69 */
70 Warden() {
71 random.setSeed(System.currentTimeMillis());
Thomas Vachuska71ff3322016-05-03 15:33:05 -070072 timer.schedule(new Reposessor(), MINUTE / 4, MINUTE / 2);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070073 }
74
75 /**
76 * Returns list of names of supported cells.
77 *
78 * @return list of cell names
79 */
80 Set<String> getCells() {
81 String[] list = supported.list();
82 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
83 }
84
85 /**
86 * Returns list of names of available cells.
87 *
88 * @return list of cell names
89 */
90 Set<String> getAvailableCells() {
91 Set<String> available = new HashSet<>(getCells());
92 available.removeAll(getReservedCells());
93 return ImmutableSet.copyOf(available);
94 }
95
96 /**
97 * Returns list of names of reserved cells.
98 *
99 * @return list of cell names
100 */
101 Set<String> getReservedCells() {
102 String[] list = reserved.list();
103 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
104 }
105
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700106 /**
107 * Returns reservation for the specified user.
108 *
109 * @param userName user name
110 * @return cell reservation record or null if user does not have one
111 */
112 Reservation currentUserReservation(String userName) {
113 checkNotNull(userName, USER_NOT_NULL);
114 for (String cellName : getReservedCells()) {
115 Reservation reservation = currentCellReservation(cellName);
116 if (reservation != null && userName.equals(reservation.userName)) {
117 return reservation;
118 }
119 }
120 return null;
121 }
122
123 /**
124 * Returns the name of the user who reserved the given cell.
125 *
126 * @param cellName cell name
127 * @return cell reservation record or null if cell is not reserved
128 */
129 Reservation currentCellReservation(String cellName) {
130 checkNotNull(cellName, CELL_NOT_NULL);
131 File cellFile = new File(reserved, cellName);
132 if (!cellFile.exists()) {
133 return null;
134 }
135 try (InputStream stream = new FileInputStream(cellFile)) {
136 return new Reservation(new String(ByteStreams.toByteArray(stream), "UTF-8"));
137 } catch (IOException e) {
138 throw new IllegalStateException("Unable to get current user for cell " + cellName, e);
139 }
140 }
141
142 /**
143 * Reserves a cell for the specified user and their public access key.
144 *
145 * @param userName user name
146 * @param sshKey user ssh public key
147 * @param minutes number of minutes for reservation
148 * @return reserved cell definition
149 */
150 synchronized String borrowCell(String userName, String sshKey, int minutes) {
151 checkNotNull(userName, USER_NOT_NULL);
152 checkNotNull(sshKey, KEY_NOT_NULL);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700153 checkArgument(minutes < MAX_MINUTES, "Number of minutes must be less than %d", MAX_MINUTES);
154 long now = System.currentTimeMillis();
155 Reservation reservation = currentUserReservation(userName);
156 if (reservation == null) {
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700157 checkArgument(minutes >= 0, "Number of minutes must be non-negative");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700158 Set<String> cells = getAvailableCells();
159 checkState(!cells.isEmpty(), "No cells are presently available");
160 String cellName = ImmutableList.copyOf(cells).get(random.nextInt(cells.size()));
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700161 reservation = new Reservation(cellName, userName, now, minutes == 0 ? DEFAULT_MINUTES : minutes);
Thomas Vachuska0d337002016-05-04 10:00:18 -0700162 } else if (minutes == 0) {
163 // If minutes are 0, simply return the cell definition
164 return getCellDefinition(reservation.cellName);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700165 } else {
166 reservation = new Reservation(reservation.cellName, userName, now, minutes);
167 }
168
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700169 reserveCell(reservation);
170 createCell(reservation, sshKey);
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700171 log(userName, reservation.cellName, "borrowed for " + reservation.duration + " minutes");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700172 return getCellDefinition(reservation.cellName);
173 }
174
175 /**
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700176 * Returns the specified cell for the specified user and their public access key.
177 *
178 * @param userName user name
179 */
180 synchronized void returnCell(String userName) {
181 checkNotNull(userName, USER_NOT_NULL);
182 Reservation reservation = currentUserReservation(userName);
183 checkState(reservation != null, "User %s has no cell reservations", userName);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700184
185 unreserveCell(reservation);
186 destroyCell(reservation);
187 log(userName, reservation.cellName, "returned");
188 }
189
190 /**
191 * Reserves the specified cell for the user the source file and writes the
192 * specified content to the target file.
193 *
194 * @param reservation cell reservation record
195 */
196 private void reserveCell(Reservation reservation) {
197 File cellFile = new File(reserved, reservation.cellName);
198 try (FileOutputStream stream = new FileOutputStream(cellFile)) {
199 stream.write(reservation.encode().getBytes(UTF_8));
200 } catch (IOException e) {
201 throw new IllegalStateException("Unable to reserve cell " + reservation.cellName, e);
202 }
203 }
204
205 private String getCellDefinition(String cellName) {
206 return exec("bin/cell-def " + cellName);
207 }
208
209 /**
210 * Cancels the specified reservation.
211 *
212 * @param reservation reservation record
213 */
214 private void unreserveCell(Reservation reservation) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700215 checkState(new File(reserved, reservation.cellName).delete(),
216 "Unable to return cell %s", reservation.cellName);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700217 }
218
219 /**
220 * Creates the cell for the specified user SSH key.
221 *
222 * @param reservation cell reservation
223 * @param sshKey ssh key
224 */
225 private void createCell(Reservation reservation, String sshKey) {
226 String cellInfo = getCellInfo(reservation.cellName);
227 String cmd = String.format("bin/create-cell %s %s %s",
228 reservation.cellName, cellInfo, sshKey);
229 exec(cmd);
230 }
231
232 /**
233 * Destroys the specified cell.
234 *
235 * @param reservation reservation record
236 */
237 private void destroyCell(Reservation reservation) {
238 exec("bin/destroy-cell " + reservation.cellName);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700239 }
240
241 /**
242 * Reads the definition of the specified cell.
243 *
244 * @param cellName cell name
245 * @return cell definition
246 */
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700247 String getCellInfo(String cellName) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700248 File cellFile = new File(supported, cellName);
249 try (InputStream stream = new FileInputStream(cellFile)) {
250 return new String(ByteStreams.toByteArray(stream), UTF_8);
251 } catch (IOException e) {
252 throw new IllegalStateException("Unable to definition for cell " + cellName, e);
253 }
254 }
255
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700256 // Executes the specified command.
257 private String exec(String command) {
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700258 try {
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700259 Process process = Runtime.getRuntime().exec(command);
260 String output = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700261 process.waitFor(TIMEOUT, TimeUnit.SECONDS);
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700262 return process.exitValue() == 0 ? output : null;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700263 } catch (Exception e) {
Thomas Vachuskae91541f2016-05-05 23:15:41 -0700264 throw new IllegalStateException("Unable to execute " + command);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700265 }
266 }
267
268 // Creates an audit log entry.
269 void log(String userName, String cellName, String action) {
270 try (FileOutputStream fos = new FileOutputStream(log, true);
271 PrintWriter pw = new PrintWriter(fos)) {
272 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
273 pw.println(String.format("%s\t%s\t%s\t%s", format.format(new Date()),
274 userName, cellName, action));
275 pw.flush();
276 } catch (IOException e) {
277 throw new IllegalStateException("Unable to log reservation action", e);
278 }
279 }
280
281 // Task for re-possessing overdue cells
282 private class Reposessor extends TimerTask {
283 @Override
284 public void run() {
285 long now = System.currentTimeMillis();
286 for (String cellName : getReservedCells()) {
287 Reservation reservation = currentCellReservation(cellName);
288 if (reservation != null &&
289 (reservation.time + reservation.duration * MINUTE) < now) {
290 try {
291 returnCell(reservation.userName);
292 } catch (Exception e) {
293 e.printStackTrace();
294 }
295 }
296 }
297 }
298 }
299}