• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / JavaSpiNNaker / 12953575522

24 Jan 2025 04:28PM UTC coverage: 38.584% (+1.3%) from 37.299%
12953575522

push

github

rowleya
Fix #1156 by including the internal in the form

9139 of 23686 relevant lines covered (38.58%)

1.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.36
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/admin/AdminControllerImpl.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
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
 *     https://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
package uk.ac.manchester.spinnaker.alloc.admin;
17

18
import static java.lang.String.format;
19
import static java.util.Arrays.stream;
20
import static java.util.Objects.nonNull;
21
import static java.util.stream.Collectors.toSet;
22
import static org.apache.commons.io.IOUtils.buffer;
23
import static org.slf4j.LoggerFactory.getLogger;
24
import static org.springframework.security.core.context.SecurityContextHolder.getContext;
25
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
26
import static org.springframework.web.servlet.support.ServletUriComponentsBuilder.fromCurrentRequestUri;
27
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.BASE_URI;
28
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.BLACKLIST_URI;
29
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.BOARDS_URI;
30
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.BOARD_VIEW;
31
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.CREATE_GROUP_URI;
32
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.CREATE_GROUP_VIEW;
33
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.CREATE_USER_URI;
34
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.CREATE_USER_VIEW;
35
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.DEFINED_MACHINES_OBJ;
36
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.GROUPS_URI;
37
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.GROUP_DETAILS_VIEW;
38
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.GROUP_LIST_VIEW;
39
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.GROUP_OBJ;
40
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.MACHINE_URI;
41
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.MACHINE_VIEW;
42
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.MAIN_VIEW;
43
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.TEMP_URI;
44
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.TRUST_LEVELS;
45
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.USERS_URI;
46
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.USER_DETAILS_VIEW;
47
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.USER_LIST_VIEW;
48
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.USER_OBJ;
49
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addBlacklist;
50
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addBoard;
51
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addCollabratoryList;
52
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addGroup;
53
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addLocalGroupList;
54
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addLocalUserList;
55
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addMachineList;
56
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addMachineReports;
57
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addMachineTagging;
58
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addNotice;
59
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addOrganisationList;
60
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addRemoteUserList;
61
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addUrl;
62
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addUser;
63
import static uk.ac.manchester.spinnaker.alloc.admin.AdminControllerSupport.addUserList;
64
import static uk.ac.manchester.spinnaker.alloc.model.GroupRecord.GroupType.COLLABRATORY;
65
import static uk.ac.manchester.spinnaker.alloc.model.GroupRecord.GroupType.INTERNAL;
66
import static uk.ac.manchester.spinnaker.alloc.model.GroupRecord.GroupType.ORGANISATION;
67
import static uk.ac.manchester.spinnaker.alloc.security.SecurityConfig.IS_ADMIN;
68
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_URI;
69
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_URI;
70
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.MAIN_URI;
71
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_URI;
72
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_URI;
73
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_PATH;
74
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_PATH;
75
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_PATH;
76
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_PATH;
77
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.error;
78
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.errorMessage;
79
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.uri;
80
import static uk.ac.manchester.spinnaker.alloc.web.SystemController.USER_MAY_CHANGE_PASSWORD;
81

82
import java.io.IOException;
83
import java.net.URI;
84
import java.security.Principal;
85
import java.util.HashMap;
86
import java.util.List;
87
import java.util.Map;
88
import java.util.Optional;
89
import java.util.stream.Collectors;
90

91
import javax.servlet.http.HttpServletRequest;
92
import javax.ws.rs.WebApplicationException;
93
import javax.ws.rs.core.Response.Status;
94

95
import org.slf4j.Logger;
96
import org.springframework.beans.factory.annotation.Autowired;
97
import org.springframework.dao.DataAccessException;
98
import org.springframework.http.HttpStatus;
99
import org.springframework.http.ResponseEntity;
100
import org.springframework.security.access.prepost.PreAuthorize;
101
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
102
import org.springframework.stereotype.Controller;
103
import org.springframework.ui.ModelMap;
104
import org.springframework.validation.BindException;
105
import org.springframework.web.bind.annotation.ExceptionHandler;
106
import org.springframework.web.method.HandlerMethod;
107
import org.springframework.web.multipart.MultipartFile;
108
import org.springframework.web.servlet.ModelAndView;
109
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
110

111
import uk.ac.manchester.spinnaker.alloc.ServiceConfig.URLPathMaker;
112
import uk.ac.manchester.spinnaker.alloc.admin.MachineDefinitionLoader.Machine;
113
import uk.ac.manchester.spinnaker.alloc.admin.MachineStateControl.BoardState;
114
import uk.ac.manchester.spinnaker.alloc.allocator.QuotaManager;
115
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI;
116
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
117
import uk.ac.manchester.spinnaker.alloc.db.Row;
118
import uk.ac.manchester.spinnaker.alloc.model.BoardRecord;
119
import uk.ac.manchester.spinnaker.alloc.model.BoardTemperatures;
120
import uk.ac.manchester.spinnaker.alloc.model.GroupRecord;
121
import uk.ac.manchester.spinnaker.alloc.model.MemberRecord;
122
import uk.ac.manchester.spinnaker.alloc.model.UserRecord;
123
import uk.ac.manchester.spinnaker.alloc.security.TrustLevel;
124
import uk.ac.manchester.spinnaker.alloc.web.Action;
125
import uk.ac.manchester.spinnaker.alloc.web.SystemController;
126
import uk.ac.manchester.spinnaker.machine.board.PhysicalCoords;
127
import uk.ac.manchester.spinnaker.machine.board.TriadCoords;
128
import uk.ac.manchester.spinnaker.messages.model.Blacklist;
129

130
/**
131
 * Implements the logic supporting the JSP views and maps them into URL space.
132
 *
133
 * @author Donal Fellows
134
 */
135
@Controller("mvc.adminUI")
136
@PreAuthorize(IS_ADMIN)
137
public class AdminControllerImpl extends DatabaseAwareBean
3✔
138
                implements AdminController {
139
        private static final Logger log = getLogger(AdminControllerImpl.class);
3✔
140

141
        /** One board-hour in board-seconds. */
142
        private static final int BOARD_HOUR = 3600;
143

144
        /** Dummy map. */
145
        private static final ModelMap NULL_MAP = null;
3✔
146

147
        /** Dummy user. */
148
        private static final Principal NULL_USER = null;
3✔
149

150
        /** Dummy attributes. */
151
        private static final RedirectAttributes NULL_ATTR = null;
3✔
152

153
        @Autowired
154
        private UserControl userManager;
155

156
        @Autowired
157
        private MachineStateControl machineController;
158

159
        @Autowired
160
        private MachineDefinitionLoader machineDefiner;
161

162
        @Autowired
163
        private SpallocAPI spalloc;
164

165
        @Autowired
166
        private QuotaManager quotaManager;
167

168
        @Autowired
169
        private URLPathMaker urlMaker;
170

171
        private class MachineName {
172
                String name;
173

174
                boolean inService;
175

176
                MachineName(Row row) {
×
177
                        name = row.getString("machine_name");
×
178
                        inService = row.getBoolean("in_service");
×
179
                }
×
180
        }
181

182
        private Map<String, Boolean> getMachineNames(boolean allowOutOfService) {
183
                try (var conn = getConnection();
×
184
                                var listMachines = conn.query(LIST_MACHINE_NAMES)) {
×
185
                        return conn.transaction(false,
×
186
                                        () -> Row.stream(listMachines.call(MachineName::new,
×
187
                                                        allowOutOfService))
×
188
                                        .toMap(m -> m.name, m -> m.inService));
×
189
                } catch (DataAccessException e) {
×
190
                        log.warn("problem when listing machines", e);
×
191
                        return Map.of();
×
192
                }
193
        }
194

195
        private static AdminController admin() {
196
                // Do not refactor to a constant; request-aware!
197
                return on(AdminController.class);
3✔
198
        }
199

200
        private static SystemController system() {
201
                // Do not refactor to a constant; request-aware!
202
                return on(SystemController.class);
3✔
203
        }
204

205
        /**
206
         * All models should contain a common set of attributes that describe where
207
         * the view is rendering and where other parts of the admin interface are.
208
         * Only call from
209
         * {@link #addStandardContext(ModelAndView, RedirectAttributes)}.
210
         *
211
         * @param model
212
         *            The base model to add to. This may be the real model or the
213
         *            flash attributes.
214
         */
215
        private void addStandardContextAttrs(Map<String, Object> model) {
216
                var auth = getContext().getAuthentication();
3✔
217
                boolean mayChangePassword =
3✔
218
                                auth instanceof UsernamePasswordAuthenticationToken;
219

220
                model.put(BASE_URI, fromCurrentRequestUri().toUriString());
3✔
221
                model.put(TRUST_LEVELS, TrustLevel.values());
3✔
222
                model.put(USERS_URI, uri(admin().listUsers(NULL_MAP)));
3✔
223
                model.put(CREATE_USER_URI, uri(admin().getUserCreationForm(NULL_MAP)));
3✔
224
                model.put(CREATE_GROUP_URI,
3✔
225
                                uri(admin().getGroupCreationForm(NULL_MAP)));
3✔
226
                model.put(GROUPS_URI, uri(admin().listGroups(NULL_MAP)));
3✔
227
                model.put(BOARDS_URI, uri(admin().boards(NULL_MAP)));
3✔
228
                model.put(MACHINE_URI, uri(admin().machineManagement(NULL_MAP)));
3✔
229
                model.put(MAIN_URI, uri(system().index()));
3✔
230
                model.put(CHANGE_PASSWORD_URI, urlMaker.systemUrl(
3✔
231
                                CHANGE_PASSWORD_PATH));
232
                model.put(LOGOUT_URI, urlMaker.systemUrl(LOGOUT_PATH));
3✔
233
                model.put(SPALLOC_CSS_URI, urlMaker.systemUrl(SPALLOC_CSS_PATH));
3✔
234
                model.put(SPALLOC_JS_URI, urlMaker.systemUrl(SPALLOC_JS_PATH));
3✔
235
                model.put(USER_MAY_CHANGE_PASSWORD, mayChangePassword);
3✔
236
        }
3✔
237

238
        /**
239
         * All models should contain a common set of attributes that describe where
240
         * the view is rendering and where other parts of the admin interface are.
241
         *
242
         * @param mav
243
         *            The model-and-view.
244
         * @param attrs
245
         *            The redirect attributes, or {@code null} if this is not a
246
         *            redirect.
247
         * @return The enhanced model-and-view.
248
         */
249
        private ModelAndView addStandardContext(ModelAndView mav,
250
                        RedirectAttributes attrs) {
251
                addStandardContextAttrs(nonNull(attrs)
3✔
252
                                // Real implementation of flash attrs is always a ModelMap
253
                                ? (ModelMap) attrs.getFlashAttributes()
3✔
254
                                : mav.getModel());
3✔
255
                return mav;
3✔
256
        }
257

258
        /**
259
         * All models should contain a common set of attributes that describe where
260
         * the view is rendering and where other parts of the admin interface are.
261
         *
262
         * @param mav
263
         *            The model-and-view.
264
         * @return The enhanced model-and-view.
265
         */
266
        private ModelAndView addStandardContext(ModelAndView mav) {
267
                return addStandardContext(mav, null);
3✔
268
        }
269

270
        /**
271
         * Construct a model-and-view which will redirect to a target URL. All
272
         * models should contain a common set of attributes that describe where the
273
         * view is rendering and where other parts of the admin interface are.
274
         *
275
         * @param uri
276
         *            The URI to redirect to. Only the path is currently used.
277
         * @param attrs
278
         *            The redirect attributes, or {@code null} if this is not a
279
         *            redirect.
280
         * @return The model-and-view.
281
         */
282
        private ModelAndView redirectTo(URI uri, RedirectAttributes attrs) {
283
                return addStandardContext(new ModelAndView("redirect:" + uri.getPath()),
3✔
284
                                attrs);
285
        }
286

287
        private ModelAndView errors(String message) {
288
                return addStandardContext(error(message), null);
×
289
        }
290

291
        private ModelAndView errors(DataAccessException exception) {
292
                return addStandardContext(error("database access failed: "
×
293
                                + exception.getMostSpecificCause().getMessage()), null);
×
294
        }
295

296
        @ExceptionHandler(BindException.class)
297
        ModelAndView validationError(BindException result, HttpServletRequest req) {
298
                log.debug("Binding problem on request {}", req.getRequestURI());
×
299
                if (req.getMethod() == "POST") {
×
300
                        try {
301
                                log.debug("Body: {}", req.getReader().lines().collect(
×
302
                                                Collectors.joining()));
×
303
                        } catch (IOException e) {
×
304
                                log.debug("Failed to read request body", e);
×
305
                        }
×
306
                }
307
                if (result.hasGlobalErrors()) {
×
308
                        // I don't believe this is really reachable code
309
                        log.debug("global binding problem", result);
×
310
                        return errors(errorMessage(result.getGlobalError()));
×
311
                } else if (result.hasFieldErrors()) {
×
312
                        log.debug("field binding problem", result);
×
313
                        return errors(errorMessage(result.getFieldError()));
×
314
                } else {
315
                        // This should definitely be unreachable
316
                        log.error("unknown binding error", result);
×
317
                        return errors("unknown error");
×
318
                }
319
        }
320

321
        /**
322
         * Convert thrown exceptions from the DB layer to views so the rest of the
323
         * code doesn't have to.
324
         *
325
         * @param e
326
         *            A database access exception.
327
         * @param hm
328
         *            What method generated the problem? Used to look up what was
329
         *            supposed to happen.
330
         * @return The view to render.
331
         */
332
        @ExceptionHandler(DataAccessException.class)
333
        ModelAndView dbException(DataAccessException e, HandlerMethod hm) {
334
                var a = hm.getMethodAnnotation(Action.class);
×
335
                if (nonNull(a)) {
×
336
                        log.warn("database access issue when {}", a.value(), e);
×
337
                } else {
338
                        log.warn("database access issue", e);
×
339
                }
340
                return errors(e);
×
341
        }
342

343
        private static class AdminException extends RuntimeException {
344
                private static final long serialVersionUID = 8401068773689159840L;
345

346
                AdminException(String message) {
347
                        super(message);
×
348
                }
×
349
        }
350

351
        private static final class NoUser extends AdminException {
352
                private static final long serialVersionUID = 6430674580385445089L;
353

354
                private NoUser() {
355
                        super("no such user");
×
356
                }
×
357
        }
358

359
        private static final class NoGroup extends AdminException {
360
                private static final long serialVersionUID = -4593707687103047377L;
361

362
                private NoGroup() {
363
                        super("no such group");
×
364
                }
×
365
        }
366

367
        private static final class NoBoard extends AdminException {
368
                private static final long serialVersionUID = -4017368969526085002L;
369

370
                private NoBoard() {
371
                        super("no such board");
×
372
                }
×
373
        }
374

375
        /**
376
         * Convert thrown admin issues to views so the rest of the code doesn't have
377
         * to.
378
         *
379
         * @param e
380
         *            An admin exception.
381
         * @param hm
382
         *            What method generated the problem? Used to look up what was
383
         *            supposed to happen.
384
         * @return The view to render.
385
         */
386
        @ExceptionHandler(AdminException.class)
387
        ModelAndView adminException(AdminException e, HandlerMethod hm) {
388
                var a = hm.getMethodAnnotation(Action.class);
×
389
                if (nonNull(a)) {
×
390
                        log.warn("general issue when {}", a.value(), e);
×
391
                } else {
392
                        log.warn("general issue", e);
×
393
                }
394
                return errors(e.getMessage());
×
395
        }
396

397
        // The actual controller methods
398

399
        @Override
400
        @Action("getting the main admin UI")
401
        public ModelAndView mainUI(ModelMap ignored) {
402
                return addStandardContext(MAIN_VIEW.view());
×
403
        }
404

405
        @Override
406
        @Action("listing the users")
407
        public ModelAndView listUsers(ModelMap ignored) {
408
                var mav = USER_LIST_VIEW.view();
×
409
                addLocalUserList(mav,
×
410
                                userManager.listUsers(true, this::showUserFormUrl));
×
411
                addRemoteUserList(mav,
×
412
                                userManager.listUsers(false, this::showUserFormUrl));
×
413
                return addStandardContext(mav);
×
414
        }
415

416
        @Override
417
        @Action("getting the user-creation UI")
418
        public ModelAndView getUserCreationForm(ModelMap ignored) {
419
                var userForm = new UserRecord();
×
420
                userForm.setInternal(true);
×
421
                return addStandardContext(
×
422
                                CREATE_USER_VIEW.view(USER_OBJ, userForm));
×
423
        }
424

425
        @Override
426
        @Action("creating a user")
427
        public ModelAndView createUser(UserRecord user, ModelMap model,
428
                        RedirectAttributes attrs) {
429
                user.initCreationDefaults();
3✔
430
                var realUser = userManager.createUser(user)
3✔
431
                                .orElseThrow(() -> new AdminException(
3✔
432
                                                "user creation failed (duplicate username?)"));
433
                int id = realUser.getUserId();
3✔
434
                log.info("created user ID={} username={}", id, realUser.getUserName());
3✔
435
                addNotice(attrs, "created " + realUser.getUserName());
3✔
436
                return redirectTo(showUserFormUrl(id), attrs);
3✔
437
        }
438

439
        @Override
440
        @Action("getting info about a user")
441
        public ModelAndView showUserForm(int id) {
442
                var mav = USER_DETAILS_VIEW.view();
×
443
                var user = userManager.getUser(id, this::showGroupInfoUrl)
×
444
                                .orElseThrow(NoUser::new);
×
445
                addUser(mav, user);
×
446
                addUrl(mav, "deleteUri", deleteUserUrl(id));
×
447
                return addStandardContext(mav);
×
448
        }
449

450
        /**
451
         * Get URL for calling {@link #showUserForm(int) showUserForm()} later.
452
         *
453
         * @param id
454
         *            User ID
455
         * @return URL
456
         */
457
        private URI showUserFormUrl(int id) {
458
                return uri(admin().showUserForm(id));
3✔
459
        }
460

461
        /**
462
         * Get URL for calling {@link #showUserForm(int) showUserForm()} later.
463
         *
464
         * @param member
465
         *            Record referring to user
466
         * @return URL
467
         */
468
        private URI showUserFormUrl(MemberRecord member) {
469
                return uri(admin().showUserForm(member.getUserId()));
×
470
        }
471

472
        /**
473
         * Get URL for calling {@link #showUserForm(int) showUserForm()} later.
474
         *
475
         * @param user
476
         *            Record referring to user
477
         * @return URL
478
         */
479
        private URI showUserFormUrl(UserRecord user) {
480
                return uri(admin().showUserForm(user.getUserId()));
×
481
        }
482

483
        @Override
484
        @Action("updating a user's details")
485
        public ModelAndView submitUserForm(int id, UserRecord user, ModelMap model,
486
                        Principal principal) {
487
                var adminUser = principal.getName();
3✔
488
                user.setUserId(null);
3✔
489
                log.info("updating user ID={}", id);
3✔
490
                var mav = USER_DETAILS_VIEW.view(model);
3✔
491
                addUser(mav,
3✔
492
                                userManager
493
                                                .updateUser(id, user, adminUser, this::showGroupInfoUrl)
3✔
494
                                                .orElseThrow(NoUser::new));
3✔
495
                return addStandardContext(mav);
3✔
496
        }
497

498
        @Override
499
        @Action("deleting a user")
500
        public ModelAndView deleteUser(int id, Principal principal,
501
                        RedirectAttributes attrs) {
502
                var adminUser = principal.getName();
×
503
                var deletedUsername = userManager.deleteUser(id, adminUser).orElseThrow(
×
504
                                () -> new AdminException("could not delete that user"));
×
505
                log.info("deleted user ID={} username={}", id, deletedUsername);
×
506
                // Not sure that these are the correct place
507
                var mav = redirectTo(uri(admin().listUsers(NULL_MAP)), attrs);
×
508
                addNotice(attrs, "deleted " + deletedUsername);
×
509
                addUser(attrs, new UserRecord());
×
510
                return mav;
×
511
        }
512

513
        /**
514
         * Get URL for calling
515
         * {@link #deleteUser(int, Principal, RedirectAttributes) deleteUser()}
516
         * later.
517
         *
518
         * @param id
519
         *            User ID
520
         * @return URL
521
         */
522
        private URI deleteUserUrl(int id) {
523
                return uri(admin().deleteUser(id, NULL_USER, NULL_ATTR));
×
524
        }
525

526
        @Override
527
        @Action("listing the groups")
528
        public ModelAndView listGroups(ModelMap ignored) {
529
                var mav = GROUP_LIST_VIEW.view();
×
530
                addLocalGroupList(mav,
×
531
                                userManager.listGroups(INTERNAL, this::showGroupInfoUrl));
×
532
                addOrganisationList(mav,
×
533
                                userManager.listGroups(ORGANISATION, this::showGroupInfoUrl));
×
534
                addCollabratoryList(mav,
×
535
                                userManager.listGroups(COLLABRATORY, this::showGroupInfoUrl));
×
536
                return addStandardContext(mav);
×
537
        }
538

539
        @Override
540
        @Action("getting info about a group")
541
        public ModelAndView showGroupInfo(int id) {
542
                var mav = GROUP_DETAILS_VIEW.view();
×
543
                var userLocations = new HashMap<String, URI>();
×
544
                addGroup(mav, userManager.getGroup(id, m -> {
×
545
                        userLocations.put(m.getUserName(), showUserFormUrl(m));
×
546
                        return uri(
×
547
                                        admin().removeUserFromGroup(id, m.getUserId(), NULL_ATTR));
×
548
                }).orElseThrow(NoGroup::new));
×
549
                addUserList(mav, userLocations);
×
550
                addUrl(mav, "deleteUri", uri(admin().deleteGroup(id, NULL_ATTR)));
×
551
                addUrl(mav, "addUserUri",
×
552
                                uri(admin().addUserToGroup(id, null, NULL_ATTR)));
×
553
                addUrl(mav, "addQuotaUri",
×
554
                                uri(admin().adjustGroupQuota(id, 0, NULL_ATTR)));
×
555
                return addStandardContext(mav);
×
556
        }
557

558
        /**
559
         * Get URL for calling {@link #showGroupInfo(int) showGroupInfo()} later.
560
         *
561
         * @param id
562
         *            Group ID
563
         * @return URL
564
         */
565
        private URI showGroupInfoUrl(int id) {
566
                return uri(admin().showGroupInfo(id));
×
567
        }
568

569
        /**
570
         * Get URL for calling {@link #showGroupInfo(int) showGroupInfo()} later.
571
         *
572
         * @param membership
573
         *            Record referring to group
574
         * @return URL
575
         */
576
        private URI showGroupInfoUrl(MemberRecord membership) {
577
                return uri(admin().showGroupInfo(membership.getGroupId()));
×
578
        }
579

580
        /**
581
         * Get URL for calling {@link #showGroupInfo(int) showGroupInfo()} later.
582
         *
583
         * @param group
584
         *            Record referring to group
585
         * @return URL
586
         */
587
        private URI showGroupInfoUrl(GroupRecord group) {
588
                return uri(admin().showGroupInfo(group.getGroupId()));
×
589
        }
590

591
        @Override
592
        @Action("getting the group-creation UI")
593
        public ModelAndView getGroupCreationForm(ModelMap ignored) {
594
                return addStandardContext(
×
595
                                CREATE_GROUP_VIEW.view(GROUP_OBJ, new CreateGroupModel()));
×
596
        }
597

598
        @Override
599
        @Action("creating a group")
600
        public ModelAndView createGroup(CreateGroupModel groupRequest,
601
                        RedirectAttributes attrs) {
602
                var realGroup =
×
603
                                userManager.createGroup(groupRequest.toGroupRecord(), INTERNAL)
×
604
                                                .orElseThrow(() -> new AdminException(
×
605
                                                                "group creation failed (duplicate name?)"));
606
                int id = realGroup.getGroupId();
×
607
                log.info("created group ID={} name={}", id, realGroup.getGroupName());
×
608
                addNotice(attrs, "created " + realGroup.getGroupName());
×
609
                return redirectTo(showGroupInfoUrl(id), attrs);
×
610
        }
611

612
        @Override
613
        @Action("adding a user to a group")
614
        public ModelAndView addUserToGroup(int id, String user,
615
                        RedirectAttributes attrs) {
616
                var g = userManager.getGroup(id, null).orElseThrow(NoGroup::new);
×
617
                var u = userManager.getUser(user, null).orElseThrow(NoUser::new);
×
618
                if (userManager.addUserToGroup(u, g).isPresent()) {
×
619
                        log.info("added user {} to group {}", u.getUserName(),
×
620
                                        g.getGroupName());
×
621
                        addNotice(attrs, format("added user %s to group %s",
×
622
                                        u.getUserName(), g.getGroupName()));
×
623
                } else {
624
                        addNotice(attrs, format("user %s is already a member of group %s",
×
625
                                        u.getUserName(), g.getGroupName()));
×
626
                }
627
                return redirectTo(showGroupInfoUrl(id), attrs);
×
628
        }
629

630
        @Override
631
        @Action("removing a user from a group")
632
        public ModelAndView removeUserFromGroup(int id, int userid,
633
                        RedirectAttributes attrs) {
634
                var g = userManager.getGroup(id, null).orElseThrow(NoGroup::new);
×
635
                var u = userManager.getUser(userid, null).orElseThrow(NoUser::new);
×
636
                if (userManager.removeUserFromGroup(u, g)) {
×
637
                        log.info("removed user {} from group {}", u.getUserName(),
×
638
                                        g.getGroupName());
×
639
                        addNotice(attrs, format("removed user %s from group %s",
×
640
                                        u.getUserName(), g.getGroupName()));
×
641
                } else {
642
                        addNotice(attrs,
×
643
                                        format("user %s is already not a member of group %s",
×
644
                                                        u.getUserName(), g.getGroupName()));
×
645
                }
646
                return redirectTo(showGroupInfoUrl(id), attrs);
×
647
        }
648

649
        @Override
650
        @Action("adjusting a group's quota")
651
        public ModelAndView adjustGroupQuota(int id, int delta,
652
                        RedirectAttributes attrs) {
653
                quotaManager.addQuota(id, delta * BOARD_HOUR).ifPresent(aq -> {
×
654
                        log.info("adjusted quota for group {} to {}", aq.getName(),
×
655
                                        aq.getQuota());
×
656
                        // addNotice(attrs, "quota updated");
657
                });
×
658
                return redirectTo(showGroupInfoUrl(id), attrs);
×
659
        }
660

661
        @Override
662
        @Action("deleting a group")
663
        public ModelAndView deleteGroup(int id, RedirectAttributes attrs) {
664
                var deletedGroupName =
×
665
                                userManager.deleteGroup(id).orElseThrow(NoGroup::new);
×
666
                log.info("deleted group ID={} groupname={}", id, deletedGroupName);
×
667
                addNotice(attrs, "deleted " + deletedGroupName);
×
668
                return redirectTo(uri(admin().listGroups(NULL_MAP)), attrs);
×
669
        }
670

671
        @Override
672
        @Action("getting the UI for finding boards")
673
        public ModelAndView boards(ModelMap ignored) {
674
                var mav = BOARD_VIEW.view();
×
675
                addBoard(mav, new BoardRecord());
×
676
                addMachineList(mav, getMachineNames(true));
×
677
                return addStandardContext(mav);
×
678
        }
679

680
        private Optional<BoardState> getBoardState(BoardRecord board) {
681
                if (nonNull(board.getId())) {
×
682
                        return machineController.findId(board.getId());
×
683
                } else if (board.isTriadCoordPresent()) {
×
684
                        return machineController.findTriad(board.getMachineName(),
×
685
                                        new TriadCoords(board.getX(), board.getY(), board.getZ()));
×
686
                } else if (board.isPhysicalCoordPresent()) {
×
687
                        return machineController.findPhysical(board.getMachineName(),
×
688
                                        new PhysicalCoords(board.getCabinet(), board.getFrame(),
×
689
                                                        board.getBoard()));
×
690
                } else if (board.isAddressPresent()) {
×
691
                        return machineController.findIP(board.getMachineName(),
×
692
                                        board.getIpAddress());
×
693
                } else {
694
                        // unreachable because of validation
695
                        return Optional.empty();
×
696
                }
697
        }
698

699
        // TODO should we refactor into multiple methods?
700
        @Override
701
        @Action("processing changes to a board's configuration")
702
        public ModelAndView board(BoardRecord board, ModelMap model) {
703
                var bs = getBoardState(board).orElseThrow(NoBoard::new);
×
704

705
                inflateBoardRecord(board, bs);
×
706

707
                if (board.isEnabledDefined()) {
×
708
                        // We're doing a set
709
                        log.info("setting board-allocatable state for board {} to {}", bs,
×
710
                                        board.isEnabled());
×
711
                        bs.setState(board.isEnabled());
×
712
                        spalloc.purgeDownCache();
×
713
                }
714

715
                board.setEnabled(bs.getState());
×
716
                addBlacklistData(bs, model);
×
717
                inflateBoardRecord(board, bs);
×
718
                addBoard(model, board);
×
719
                addMachineList(model, getMachineNames(true));
×
720
                addUrl(model, TEMP_URI,        uri(admin().getTemperatures(
×
721
                                board.getId(), board.getBmpId())));
×
722
                return addStandardContext(BOARD_VIEW.view(model));
×
723
        }
724

725
        @Override
726
        @Action("saving changes to a board blacklist")
727
        public ResponseEntity<Void> blacklistSave(BlacklistData bldata) {
728

729
                if (bldata.isPresent()) {
×
730
                        try {
731
                                var blacklist = bldata.getParsedBlacklist();
×
732
                                machineController.writeBlacklistToDB(bldata.getBoardId(),
×
733
                                                blacklist);
734
                                machineController.writeBlacklistToMachine(bldata.getBoardId(),
×
735
                                                bldata.getBmpId(), blacklist);
×
736
                        } catch (InterruptedException e) {
×
737
                                throw new WebApplicationException(e);
×
738
                        }
×
739
                }
740
                return new ResponseEntity<>(HttpStatus.OK);
×
741
        }
742

743
        @Override
744
        @Action("fetching a live board blacklist from the machine")
745
        public BlacklistData blacklistFetch(int boardId, int bmpId) {
746

747
                log.info("pulling blacklist from board {}", boardId);
×
748
                BlacklistData data = new BlacklistData();
×
749
                data.setBoardId(boardId);
×
750
                data.setBmpId(bmpId);
×
751
                machineController.pullBlacklist(boardId, bmpId).ifPresentOrElse(bl -> {
×
752
                        data.setPresent(true);
×
753
                        data.setSynched(true);
×
754
                        data.setBlacklist(bl.render());
×
755
                }, () -> {
×
756
                        data.setPresent(false);
×
757
                        data.setSynched(false);
×
758
                });
×
759

760
                return data;
×
761
        }
762

763
        @Override
764
        @Action("getting board temperature data from the machine")
765
        public BoardTemperatures getTemperatures(int boardId, int bmpId) {
766
                try {
767
                        return new BoardTemperatures(
×
768
                                        machineController.readTemperatureFromMachine(boardId, bmpId)
×
769
                                                .orElseThrow(() -> new WebApplicationException(
×
770
                                                                Status.NOT_FOUND)));
771
                } catch (InterruptedException e) {
×
772
                        throw new WebApplicationException(e);
×
773
                }
774
        }
775

776
        /**
777
         * Add current blacklist data to model.
778
         *
779
         * @param board
780
         *            Which board's blacklist data to add.
781
         * @param model
782
         *            The model to add it to.
783
         */
784
        private void addBlacklistData(BoardState board, ModelMap model) {
785
                var bldata = new BlacklistData();
×
786
                bldata.setBoardId(board.id);
×
787
                var bl = machineController.readBlacklistFromDB(board);
×
788
                bldata.setPresent(bl.isPresent());
×
789
                bl.map(Blacklist::render).ifPresent(bldata::setBlacklist);
×
790
                bldata.setSynched(machineController.isBlacklistSynched(board));
×
791

792
                addBlacklist(model, bldata);
×
793
                addUrl(model, BLACKLIST_URI,
×
794
                                uri(admin().blacklistFetch(board.id, board.bmpId)));
×
795
        }
×
796

797
        /**
798
         * Copy settings from the record out of the DB.
799
         *
800
         * @param br
801
         *            Where the values are copied to. A partial state.
802
         * @param bs
803
         *            Where the values are copied from. A complete state from the
804
         *            DB.
805
         */
806
        private void inflateBoardRecord(BoardRecord br, BoardState bs) {
807
                // Inflate the coordinates
808
                br.setId(bs.id);
×
809
                br.setBmpId(bs.bmpId);
×
810
                br.setX(bs.x);
×
811
                br.setY(bs.y);
×
812
                br.setZ(bs.z);
×
813
                br.setCabinet(bs.cabinet);
×
814
                br.setFrame(bs.frame);
×
815
                br.setBoard(bs.board);
×
816
                br.setIpAddress(bs.address);
×
817
                br.setMachineName(bs.machineName);
×
818

819
                // Inflate the other properties
820
                br.setPowered(bs.getPower());
×
821
                br.setLastPowerOff(bs.getPowerOffTime().orElse(null));
×
822
                br.setLastPowerOn(bs.getPowerOnTime().orElse(null));
×
823
                br.setJobId(bs.getAllocatedJob().orElse(null));
×
824
                br.setReports(bs.getReports());
×
825
        }
×
826

827
        /**
828
         * {@inheritDoc}
829
         * <p>
830
         * <strong>Implementation note:</strong> This is the baseline information
831
         * that {@code admin/machine.jsp} needs.
832
         */
833
        @Override
834
        @Action("getting a machine's configuration")
835
        public ModelAndView machineManagement(ModelMap ignored) {
836
                var mav = MACHINE_VIEW.view();
×
837
                addMachineList(mav, getMachineNames(true));
×
838
                var tagging = machineController.getMachineTagging();
×
839
                tagging.forEach(
×
840
                                t -> t.setUrl(uri(system().getMachineInfo(t.getName()))));
×
841
                addMachineTagging(mav, tagging);
×
842
                addMachineReports(mav, machineController.getMachineReports());
×
843
                return addStandardContext(mav);
×
844
        }
845

846
        @Override
847
        @Action("retagging a machine")
848
        public ModelAndView retagMachine(String machineName, String newTags) {
849
                var tags =
×
850
                                stream(newTags.split(",")).map(String::strip).collect(toSet());
×
851
                machineController.updateTags(machineName, tags);
×
852
                log.info("retagged {} to have tags {}", machineName, tags);
×
853
                return machineManagement(NULL_MAP);
×
854
        }
855

856
        @Override
857
        @Action("disabling a machine")
858
        public ModelAndView disableMachine(String machineName) {
859
                machineController.setMachineState(machineName, false);
×
860
                log.info("marked {} as out of service", machineName);
×
861
                return machineManagement(NULL_MAP);
×
862
        }
863

864
        @Override
865
        @Action("enabling a machine")
866
        public ModelAndView enableMachine(String machineName) {
867
                machineController.setMachineState(machineName, true);
×
868
                log.info("marked {} as in service", machineName);
×
869
                return machineManagement(NULL_MAP);
×
870
        }
871

872
        @Override
873
        @Action("defining a machine")
874
        public ModelAndView defineMachine(MultipartFile file) {
875
                var machines = extractMachineDefinitions(file);
×
876
                for (var m : machines) {
×
877
                        machineDefiner.loadMachineDefinition(m);
×
878
                        log.info("defined machine {}", m.getName());
×
879
                }
×
880
                var mav = machineManagement(NULL_MAP);
×
881
                // Tailor with extra objects here
882
                mav.addObject(DEFINED_MACHINES_OBJ, machines);
×
883
                return mav;
×
884
        }
885

886
        private List<Machine> extractMachineDefinitions(MultipartFile file) {
887
                try (var input = buffer(file.getInputStream())) {
×
888
                        return machineDefiner.readMachineDefinitions(input);
×
889
                } catch (IOException e) {
×
890
                        throw new AdminException(
×
891
                                        "problem with processing file: " + e.getMessage());
×
892
                }
893
        }
894
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc