blob: 6f6ada5a86218d259f00c1b3b5328347181ebab4 [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;
32import java.util.List;
33import java.util.Random;
34import java.util.Set;
35import java.util.Timer;
36import java.util.TimerTask;
37import java.util.concurrent.TimeUnit;
38import java.util.regex.Matcher;
39import java.util.regex.Pattern;
40
41import static com.google.common.base.Preconditions.*;
42
43/**
44 * Warden for tracking use of shared test cells.
45 */
46class Warden {
47
48 private static final String CELL_NOT_NULL = "Cell name cannot be null";
49 private static final String USER_NOT_NULL = "User name cannot be null";
50 private static final String KEY_NOT_NULL = "User key cannot be null";
51 private static final String UTF_8 = "UTF-8";
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070052
53 private static final String AUTHORIZED_KEYS = "authorized_keys";
54
Thomas Vachuska0d337002016-05-04 10:00:18 -070055 private static final long TIMEOUT = 10; // 10 seconds
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070056 private static final int MAX_MINUTES = 240; // 4 hours max
57 private static final int MINUTE = 60_000; // 1 minute
Thomas Vachuskaf9872a02016-05-05 13:56:25 -070058 private static final int DEFAULT_MINUTES = 60;
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070059
60 private final File log = new File("warden.log");
61
62 private final File cells = new File("cells");
63 private final File supported = new File(cells, "supported");
64 private final File reserved = new File(cells, "reserved");
65
66 private final Random random = new Random();
67
68 private final Timer timer = new Timer("cell-pruner", true);
69
70 /**
71 * Creates a new cell warden.
72 */
73 Warden() {
74 random.setSeed(System.currentTimeMillis());
Thomas Vachuska71ff3322016-05-03 15:33:05 -070075 timer.schedule(new Reposessor(), MINUTE / 4, MINUTE / 2);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -070076 }
77
78 /**
79 * Returns list of names of supported cells.
80 *
81 * @return list of cell names
82 */
83 Set<String> getCells() {
84 String[] list = supported.list();
85 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
86 }
87
88 /**
89 * Returns list of names of available cells.
90 *
91 * @return list of cell names
92 */
93 Set<String> getAvailableCells() {
94 Set<String> available = new HashSet<>(getCells());
95 available.removeAll(getReservedCells());
96 return ImmutableSet.copyOf(available);
97 }
98
99 /**
100 * Returns list of names of reserved cells.
101 *
102 * @return list of cell names
103 */
104 Set<String> getReservedCells() {
105 String[] list = reserved.list();
106 return list != null ? ImmutableSet.copyOf(list) : ImmutableSet.of();
107 }
108
109
110 /**
111 * Returns reservation for the specified user.
112 *
113 * @param userName user name
114 * @return cell reservation record or null if user does not have one
115 */
116 Reservation currentUserReservation(String userName) {
117 checkNotNull(userName, USER_NOT_NULL);
118 for (String cellName : getReservedCells()) {
119 Reservation reservation = currentCellReservation(cellName);
120 if (reservation != null && userName.equals(reservation.userName)) {
121 return reservation;
122 }
123 }
124 return null;
125 }
126
127 /**
128 * Returns the name of the user who reserved the given cell.
129 *
130 * @param cellName cell name
131 * @return cell reservation record or null if cell is not reserved
132 */
133 Reservation currentCellReservation(String cellName) {
134 checkNotNull(cellName, CELL_NOT_NULL);
135 File cellFile = new File(reserved, cellName);
136 if (!cellFile.exists()) {
137 return null;
138 }
139 try (InputStream stream = new FileInputStream(cellFile)) {
140 return new Reservation(new String(ByteStreams.toByteArray(stream), "UTF-8"));
141 } catch (IOException e) {
142 throw new IllegalStateException("Unable to get current user for cell " + cellName, e);
143 }
144 }
145
146 /**
147 * Reserves a cell for the specified user and their public access key.
148 *
149 * @param userName user name
150 * @param sshKey user ssh public key
151 * @param minutes number of minutes for reservation
152 * @return reserved cell definition
153 */
154 synchronized String borrowCell(String userName, String sshKey, int minutes) {
155 checkNotNull(userName, USER_NOT_NULL);
156 checkNotNull(sshKey, KEY_NOT_NULL);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700157 checkArgument(minutes < MAX_MINUTES, "Number of minutes must be less than %d", MAX_MINUTES);
158 long now = System.currentTimeMillis();
159 Reservation reservation = currentUserReservation(userName);
160 if (reservation == null) {
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700161 checkArgument(minutes >= 0, "Number of minutes must be non-negative");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700162 Set<String> cells = getAvailableCells();
163 checkState(!cells.isEmpty(), "No cells are presently available");
164 String cellName = ImmutableList.copyOf(cells).get(random.nextInt(cells.size()));
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700165 reservation = new Reservation(cellName, userName, now, minutes == 0 ? DEFAULT_MINUTES : minutes);
Thomas Vachuska0d337002016-05-04 10:00:18 -0700166 } else if (minutes == 0) {
167 // If minutes are 0, simply return the cell definition
168 return getCellDefinition(reservation.cellName);
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700169 } else {
170 reservation = new Reservation(reservation.cellName, userName, now, minutes);
171 }
172
173 reserveCell(reservation.cellName, reservation);
174 installUserKeys(reservation.cellName, userName, sshKey);
Thomas Vachuskaf9872a02016-05-05 13:56:25 -0700175 log(userName, reservation.cellName, "borrowed for " + reservation.duration + " minutes");
Thomas Vachuska1eff3a62016-05-03 01:07:24 -0700176 return getCellDefinition(reservation.cellName);
177 }
178
179 /**
180 * Reserves the specified cell for the user the source file and writes the
181 * specified content to the target file.
182 *
183 * @param cellName cell name
184 * @param reservation cell reservation record
185 */
186 private void reserveCell(String cellName, Reservation reservation) {
187 try (FileOutputStream stream = new FileOutputStream(new File(reserved, cellName))) {
188 stream.write(reservation.encode().getBytes(UTF_8));
189 } catch (IOException e) {
190 throw new IllegalStateException("Unable to reserve cell " + cellName, e);
191 }
192 }
193
194 /**
195 * Returns the specified cell for the specified user and their public access key.
196 *
197 * @param userName user name
198 */
199 synchronized void returnCell(String userName) {
200 checkNotNull(userName, USER_NOT_NULL);
201 Reservation reservation = currentUserReservation(userName);
202 checkState(reservation != null, "User %s has no cell reservations", userName);
203 checkState(new File(reserved, reservation.cellName).delete(),
204 "Unable to return cell %s", reservation.cellName);
205 uninstallUserKeys(reservation.cellName);
206 log(userName, reservation.cellName, "returned");
207 }
208
209 /**
210 * Reads the definition of the specified cell.
211 *
212 * @param cellName cell name
213 * @return cell definition
214 */
215 String getCellDefinition(String cellName) {
216 File cellFile = new File(supported, cellName);
217 try (InputStream stream = new FileInputStream(cellFile)) {
218 return new String(ByteStreams.toByteArray(stream), UTF_8);
219 } catch (IOException e) {
220 throw new IllegalStateException("Unable to definition for cell " + cellName, e);
221 }
222 }
223
224 // Returns list of cell hosts, i.e. OC#, OCN
225 private List<String> cellHosts(String cellName) {
226 ImmutableList.Builder<String> builder = ImmutableList.builder();
227 Pattern pattern = Pattern.compile("export OC[0-9N]=(.*)");
228 for (String line : getCellDefinition(cellName).split("\n")) {
229 Matcher matcher = pattern.matcher(line);
230 if (matcher.matches()) {
231 builder.add(matcher.group(1).replaceAll("[\"']", ""));
232 }
233 }
234 return builder.build();
235 }
236
237 // Installs the specified user's key on all hosts of the given cell.
238 private void installUserKeys(String cellName, String userName, String sshKey) {
239 File authKeysFile = authKeys(sshKey);
240 for (String host : cellHosts(cellName)) {
241 installAuthorizedKey(host, authKeysFile.getPath());
242 }
243 checkState(authKeysFile.delete(), "Unable to install user keys");
244 }
245
246 // Uninstalls the user keys on the specified cell
247 private void uninstallUserKeys(String cellName) {
248 for (String host : cellHosts(cellName)) {
249 installAuthorizedKey(host, AUTHORIZED_KEYS);
250 }
251 }
252
253 // Installs the authorized keys on the specified host.
254 private void installAuthorizedKey(String host, String authorizedKeysFile) {
255 String cmd = "scp " + authorizedKeysFile + " sdn@" + host + ":.ssh/authorized_keys";
256 try {
257 Process process = Runtime.getRuntime().exec(cmd);
258 process.waitFor(TIMEOUT, TimeUnit.SECONDS);
259 } catch (Exception e) {
260 throw new IllegalStateException("Unable to set authorized keys for host " + host);
261 }
262 }
263
264 // Returns the file containing authorized keys that incudes the specified key.
265 private File authKeys(String sshKey) {
266 File keysFile = new File(AUTHORIZED_KEYS);
267 try {
268 File tmp = File.createTempFile("warden-", ".auth");
269 tmp.deleteOnExit();
270 try (InputStream stream = new FileInputStream(keysFile);
271 PrintWriter output = new PrintWriter(tmp)) {
272 String baseKeys = new String(ByteStreams.toByteArray(stream), UTF_8);
273 output.println(baseKeys);
274 output.println(sshKey);
275 return tmp;
276 } catch (IOException e) {
277 throw new IllegalStateException("Unable to generate authorized keys", e);
278 }
279 } catch (IOException e) {
280 throw new IllegalStateException("Unable to generate authorized keys", e);
281 }
282 }
283
284 // Creates an audit log entry.
285 void log(String userName, String cellName, String action) {
286 try (FileOutputStream fos = new FileOutputStream(log, true);
287 PrintWriter pw = new PrintWriter(fos)) {
288 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
289 pw.println(String.format("%s\t%s\t%s\t%s", format.format(new Date()),
290 userName, cellName, action));
291 pw.flush();
292 } catch (IOException e) {
293 throw new IllegalStateException("Unable to log reservation action", e);
294 }
295 }
296
297 // Task for re-possessing overdue cells
298 private class Reposessor extends TimerTask {
299 @Override
300 public void run() {
301 long now = System.currentTimeMillis();
302 for (String cellName : getReservedCells()) {
303 Reservation reservation = currentCellReservation(cellName);
304 if (reservation != null &&
305 (reservation.time + reservation.duration * MINUTE) < now) {
306 try {
307 returnCell(reservation.userName);
308 } catch (Exception e) {
309 e.printStackTrace();
310 }
311 }
312 }
313 }
314 }
315}