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