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

SpiNNakerManchester / JavaSpiNNaker / 6310285782

26 Sep 2023 08:47AM UTC coverage: 36.367% (-0.5%) from 36.866%
6310285782

Pull #658

github

dkfellows
Merge branch 'master' into java-17
Pull Request #658: Update Java version to 17 and JEE to 9

1675 of 1675 new or added lines in 266 files covered. (100.0%)

8368 of 23010 relevant lines covered (36.37%)

0.36 hits per line

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

1.68
/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_PATH;
69
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_URI;
70
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_PATH;
71
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_URI;
72
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.MAIN_URI;
73
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_PATH;
74
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_URI;
75
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_PATH;
76
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_URI;
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.io.Serial;
84
import java.net.URI;
85
import java.security.Principal;
86
import java.util.HashMap;
87
import java.util.List;
88
import java.util.Map;
89
import java.util.Optional;
90

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

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

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

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

142
        /** Dummy map. */
143
        private static final ModelMap NULL_MAP = null;
1✔
144

145
        /** Dummy user. */
146
        private static final Principal NULL_USER = null;
1✔
147

148
        /** Dummy attributes. */
149
        private static final RedirectAttributes NULL_ATTR = null;
1✔
150

151
        @Autowired
152
        private UserControl userManager;
153

154
        @Autowired
155
        private MachineStateControl machineController;
156

157
        @Autowired
158
        private MachineDefinitionLoader machineDefiner;
159

160
        @Autowired
161
        private SpallocAPI spalloc;
162

163
        @Autowired
164
        private QuotaManager quotaManager;
165

166
        @Autowired
167
        private URLPathMaker urlMaker;
168

169
        private class MachineName {
170
                String name;
171

172
                boolean inService;
173

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

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

193
        private static AdminController admin() {
194
                // Do not refactor to a constant; request-aware!
195
                return on(AdminController.class);
×
196
        }
197

198
        private static SystemController system() {
199
                // Do not refactor to a constant; request-aware!
200
                return on(SystemController.class);
×
201
        }
202

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

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

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

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

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

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

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

294
        @ExceptionHandler(BindException.class)
295
        ModelAndView validationError(BindException result) {
296
                if (result.hasGlobalErrors()) {
×
297
                        // I don't believe this is really reachable code
298
                        log.debug("binding problem", result);
×
299
                        return errors(errorMessage(result.getGlobalError()));
×
300
                } else if (result.hasFieldErrors()) {
×
301
                        log.debug("binding problem", result);
×
302
                        return errors(errorMessage(result.getFieldError()));
×
303
                } else {
304
                        // This should definitely be unreachable
305
                        log.error("unknown binding error", result);
×
306
                        return errors("unknown error");
×
307
                }
308
        }
309

310
        /**
311
         * Convert thrown exceptions from the DB layer to views so the rest of the
312
         * code doesn't have to.
313
         *
314
         * @param e
315
         *            A database access exception.
316
         * @param hm
317
         *            What method generated the problem? Used to look up what was
318
         *            supposed to happen.
319
         * @return The view to render.
320
         */
321
        @ExceptionHandler(DataAccessException.class)
322
        ModelAndView dbException(DataAccessException e, HandlerMethod hm) {
323
                var a = hm.getMethodAnnotation(Action.class);
×
324
                if (nonNull(a)) {
×
325
                        log.warn("database access issue when {}", a.value(), e);
×
326
                } else {
327
                        log.warn("database access issue", e);
×
328
                }
329
                return errors(e);
×
330
        }
331

332
        private static class AdminException extends RuntimeException {
333
                @Serial
334
                private static final long serialVersionUID = 8401068773689159840L;
335

336
                AdminException(String message) {
337
                        super(message);
×
338
                }
×
339
        }
340

341
        private static final class NoUser extends AdminException {
342
                @Serial
343
                private static final long serialVersionUID = 6430674580385445089L;
344

345
                private NoUser() {
346
                        super("no such user");
×
347
                }
×
348
        }
349

350
        private static final class NoGroup extends AdminException {
351
                @Serial
352
                private static final long serialVersionUID = -4593707687103047377L;
353

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

359
        private static final class NoBoard extends AdminException {
360
                @Serial
361
                private static final long serialVersionUID = -4017368969526085002L;
362

363
                private NoBoard() {
364
                        super("no such board");
×
365
                }
×
366
        }
367

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

390
        // The actual controller methods
391

392
        @Override
393
        @Action("getting the main admin UI")
394
        public ModelAndView mainUI(ModelMap ignored) {
395
                return addStandardContext(MAIN_VIEW.view());
×
396
        }
397

398
        @Override
399
        @Action("listing the users")
400
        public ModelAndView listUsers(ModelMap ignored) {
401
                var mav = USER_LIST_VIEW.view();
×
402
                addLocalUserList(mav,
×
403
                                userManager.listUsers(true, this::showUserFormUrl));
×
404
                addRemoteUserList(mav,
×
405
                                userManager.listUsers(false, this::showUserFormUrl));
×
406
                return addStandardContext(mav);
×
407
        }
408

409
        @Override
410
        @Action("getting the user-creation UI")
411
        public ModelAndView getUserCreationForm(ModelMap ignored) {
412
                var userForm = new UserRecord();
×
413
                userForm.setInternal(true);
×
414
                return addStandardContext(
×
415
                                CREATE_USER_VIEW.view(USER_OBJ, userForm));
×
416
        }
417

418
        @Override
419
        @Action("creating a user")
420
        public ModelAndView createUser(UserRecord user, ModelMap model,
421
                        RedirectAttributes attrs) {
422
                user.initCreationDefaults();
×
423
                var realUser = userManager.createUser(user)
×
424
                                .orElseThrow(() -> new AdminException(
×
425
                                                "user creation failed (duplicate username?)"));
426
                int id = realUser.getUserId();
×
427
                log.info("created user ID={} username={}", id, realUser.getUserName());
×
428
                addNotice(attrs, "created " + realUser.getUserName());
×
429
                return redirectTo(showUserFormUrl(id), attrs);
×
430
        }
431

432
        @Override
433
        @Action("getting info about a user")
434
        public ModelAndView showUserForm(int id) {
435
                var mav = USER_DETAILS_VIEW.view();
×
436
                var user = userManager.getUser(id, this::showGroupInfoUrl)
×
437
                                .orElseThrow(NoUser::new);
×
438
                addUser(mav, user);
×
439
                addUrl(mav, "deleteUri", deleteUserUrl(id));
×
440
                return addStandardContext(mav);
×
441
        }
442

443
        /**
444
         * Get URL for calling {@link #showUserForm(int) showUserForm()} later.
445
         *
446
         * @param id
447
         *            User ID
448
         * @return URL
449
         */
450
        private URI showUserFormUrl(int id) {
451
                return uri(admin().showUserForm(id));
×
452
        }
453

454
        /**
455
         * Get URL for calling {@link #showUserForm(int) showUserForm()} later.
456
         *
457
         * @param member
458
         *            Record referring to user
459
         * @return URL
460
         */
461
        private URI showUserFormUrl(MemberRecord member) {
462
                return uri(admin().showUserForm(member.getUserId()));
×
463
        }
464

465
        /**
466
         * Get URL for calling {@link #showUserForm(int) showUserForm()} later.
467
         *
468
         * @param user
469
         *            Record referring to user
470
         * @return URL
471
         */
472
        private URI showUserFormUrl(UserRecord user) {
473
                return uri(admin().showUserForm(user.getUserId()));
×
474
        }
475

476
        @Override
477
        @Action("updating a user's details")
478
        public ModelAndView submitUserForm(int id, UserRecord user, ModelMap model,
479
                        Principal principal) {
480
                var adminUser = principal.getName();
×
481
                user.setUserId(null);
×
482
                log.info("updating user ID={}", id);
×
483
                var mav = USER_DETAILS_VIEW.view(model);
×
484
                addUser(mav,
×
485
                                userManager
486
                                                .updateUser(id, user, adminUser, this::showGroupInfoUrl)
×
487
                                                .orElseThrow(NoUser::new));
×
488
                return addStandardContext(mav);
×
489
        }
490

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

506
        /**
507
         * Get URL for calling
508
         * {@link #deleteUser(int, Principal, RedirectAttributes) deleteUser()}
509
         * later.
510
         *
511
         * @param id
512
         *            User ID
513
         * @return URL
514
         */
515
        private URI deleteUserUrl(int id) {
516
                return uri(admin().deleteUser(id, NULL_USER, NULL_ATTR));
×
517
        }
518

519
        @Override
520
        @Action("listing the groups")
521
        public ModelAndView listGroups(ModelMap ignored) {
522
                var mav = GROUP_LIST_VIEW.view();
×
523
                addLocalGroupList(mav,
×
524
                                userManager.listGroups(INTERNAL, this::showGroupInfoUrl));
×
525
                addOrganisationList(mav,
×
526
                                userManager.listGroups(ORGANISATION, this::showGroupInfoUrl));
×
527
                addCollabratoryList(mav,
×
528
                                userManager.listGroups(COLLABRATORY, this::showGroupInfoUrl));
×
529
                return addStandardContext(mav);
×
530
        }
531

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

551
        /**
552
         * Get URL for calling {@link #showGroupInfo(int) showGroupInfo()} later.
553
         *
554
         * @param id
555
         *            Group ID
556
         * @return URL
557
         */
558
        private URI showGroupInfoUrl(int id) {
559
                return uri(admin().showGroupInfo(id));
×
560
        }
561

562
        /**
563
         * Get URL for calling {@link #showGroupInfo(int) showGroupInfo()} later.
564
         *
565
         * @param membership
566
         *            Record referring to group
567
         * @return URL
568
         */
569
        private URI showGroupInfoUrl(MemberRecord membership) {
570
                return uri(admin().showGroupInfo(membership.getGroupId()));
×
571
        }
572

573
        /**
574
         * Get URL for calling {@link #showGroupInfo(int) showGroupInfo()} later.
575
         *
576
         * @param group
577
         *            Record referring to group
578
         * @return URL
579
         */
580
        private URI showGroupInfoUrl(GroupRecord group) {
581
                return uri(admin().showGroupInfo(group.getGroupId()));
×
582
        }
583

584
        @Override
585
        @Action("getting the group-creation UI")
586
        public ModelAndView getGroupCreationForm(ModelMap ignored) {
587
                return addStandardContext(
×
588
                                CREATE_GROUP_VIEW.view(GROUP_OBJ, new CreateGroupModel()));
×
589
        }
590

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

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

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

642
        @Override
643
        @Action("adjusting a group's quota")
644
        public ModelAndView adjustGroupQuota(int id, int delta,
645
                        RedirectAttributes attrs) {
646
                quotaManager.addQuota(id, delta * BOARD_HOUR).ifPresent(aq -> {
×
647
                        log.info("adjusted quota for group {} to {}", aq.name(),
×
648
                                        aq.quota());
×
649
                        // addNotice(attrs, "quota updated");
650
                });
×
651
                return redirectTo(showGroupInfoUrl(id), attrs);
×
652
        }
653

654
        @Override
655
        @Action("deleting a group")
656
        public ModelAndView deleteGroup(int id, RedirectAttributes attrs) {
657
                var deletedGroupName =
×
658
                                userManager.deleteGroup(id).orElseThrow(NoGroup::new);
×
659
                log.info("deleted group ID={} groupname={}", id, deletedGroupName);
×
660
                addNotice(attrs, "deleted " + deletedGroupName);
×
661
                return redirectTo(uri(admin().listGroups(NULL_MAP)), attrs);
×
662
        }
663

664
        @Override
665
        @Action("getting the UI for finding boards")
666
        public ModelAndView boards(ModelMap ignored) {
667
                var mav = BOARD_VIEW.view();
×
668
                addBoard(mav, new BoardRecord());
×
669
                addMachineList(mav, getMachineNames(true));
×
670
                return addStandardContext(mav);
×
671
        }
672

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

692
        // TODO should we refactor into multiple methods?
693
        @Override
694
        @Action("processing changes to a board's configuration")
695
        public ModelAndView board(BoardRecord board, ModelMap model) {
696
                var bs = getBoardState(board).orElseThrow(NoBoard::new);
×
697

698
                inflateBoardRecord(board, bs);
×
699

700
                if (board.isEnabledDefined()) {
×
701
                        // We're doing a set
702
                        log.info("setting board-allocatable state for board {} to {}", bs,
×
703
                                        board.isEnabled());
×
704
                        bs.setState(board.isEnabled());
×
705
                        spalloc.purgeDownCache();
×
706
                }
707

708
                board.setEnabled(bs.getState());
×
709
                addBlacklistData(bs, model);
×
710
                inflateBoardRecord(board, bs);
×
711
                addBoard(model, board);
×
712
                addMachineList(model, getMachineNames(true));
×
713
                addUrl(model, TEMP_URI,        uri(admin().getTemperatures(board.getId())));
×
714
                return addStandardContext(BOARD_VIEW.view(model));
×
715
        }
716

717
        @Override
718
        @Action("saving changes to a board blacklist")
719
        public ResponseEntity<Void> blacklistSave(BlacklistData bldata) {
720
                if (bldata.isPresent()) {
×
721
                        try {
722
                                var blacklist = bldata.getParsedBlacklist();
×
723
                                machineController.writeBlacklistToDB(bldata.getBoardId(),
×
724
                                                blacklist);
725
                                machineController.writeBlacklistToMachine(bldata.getBoardId(),
×
726
                                                bldata.getBmpId(), blacklist);
×
727
                        } catch (InterruptedException e) {
×
728
                                throw new WebApplicationException(e);
×
729
                        }
×
730
                }
731
                return new ResponseEntity<>(HttpStatus.OK);
×
732
        }
733

734
        @Override
735
        @Action("fetching a live board blacklist from the machine")
736
        public BlacklistData blacklistFetch(int boardId, int bmpId) {
737
                log.info("pulling blacklist from board {}", boardId);
×
738
                var data = new BlacklistData();
×
739
                data.setBoardId(boardId);
×
740
                data.setBmpId(bmpId);
×
741
                machineController.pullBlacklist(boardId, bmpId).ifPresentOrElse(bl -> {
×
742
                        data.setPresent(true);
×
743
                        data.setSynched(true);
×
744
                        data.setBlacklist(bl.render());
×
745
                }, () -> {
×
746
                        data.setPresent(false);
×
747
                        data.setSynched(false);
×
748
                });
×
749

750
                return data;
×
751
        }
752

753
        @Override
754
        @Action("getting board temperature data from the machine")
755
        public BoardTemperatures getTemperatures(int boardId) {
756
                try {
757
                        return new BoardTemperatures(
×
758
                                        machineController.readTemperatureFromMachine(boardId)
×
759
                                                .orElseThrow(() -> new WebApplicationException(
×
760
                                                                Status.NOT_FOUND)));
761
                } catch (InterruptedException e) {
×
762
                        throw new WebApplicationException(e);
×
763
                }
764
        }
765

766
        /**
767
         * Add current blacklist data to model.
768
         *
769
         * @param board
770
         *            Which board's blacklist data to add.
771
         * @param model
772
         *            The model to add it to.
773
         */
774
        private void addBlacklistData(BoardState board, ModelMap model) {
775
                var bldata = new BlacklistData();
×
776
                bldata.setBoardId(board.id);
×
777
                var bl = machineController.readBlacklistFromDB(board);
×
778
                bldata.setPresent(bl.isPresent());
×
779
                bl.map(Blacklist::render).ifPresent(bldata::setBlacklist);
×
780
                bldata.setSynched(machineController.isBlacklistSynched(board));
×
781

782
                addBlacklist(model, bldata);
×
783
                addUrl(model, BLACKLIST_URI,
×
784
                                uri(admin().blacklistFetch(board.id, board.bmpId)));
×
785
        }
×
786

787
        /**
788
         * Copy settings from the record out of the DB.
789
         *
790
         * @param br
791
         *            Where the values are copied to. A partial state.
792
         * @param bs
793
         *            Where the values are copied from. A complete state from the
794
         *            DB.
795
         */
796
        private void inflateBoardRecord(BoardRecord br, BoardState bs) {
797
                // Inflate the coordinates
798
                br.setId(bs.id);
×
799
                br.setBmpId(bs.bmpId);
×
800
                br.setX(bs.x);
×
801
                br.setY(bs.y);
×
802
                br.setZ(bs.z);
×
803
                br.setCabinet(bs.cabinet);
×
804
                br.setFrame(bs.frame);
×
805
                br.setBoard(bs.board);
×
806
                br.setIpAddress(bs.address);
×
807
                br.setMachineName(bs.machineName);
×
808

809
                // Inflate the other properties
810
                br.setPowered(bs.getPower());
×
811
                br.setLastPowerOff(bs.getPowerOffTime().orElse(null));
×
812
                br.setLastPowerOn(bs.getPowerOnTime().orElse(null));
×
813
                br.setJobId(bs.getAllocatedJob().orElse(null));
×
814
                br.setReports(bs.getReports());
×
815
        }
×
816

817
        /**
818
         * {@inheritDoc}
819
         * <p>
820
         * <strong>Implementation note:</strong> This is the baseline information
821
         * that {@code admin/machine.jsp} needs.
822
         */
823
        @Override
824
        @Action("getting a machine's configuration")
825
        public ModelAndView machineManagement(ModelMap ignored) {
826
                var mav = MACHINE_VIEW.view();
×
827
                addMachineList(mav, getMachineNames(true));
×
828
                var tagging = machineController.getMachineTagging();
×
829
                tagging.forEach(
×
830
                                t -> t.setUrl(uri(system().getMachineInfo(t.getName()))));
×
831
                addMachineTagging(mav, tagging);
×
832
                addMachineReports(mav, machineController.getMachineReports());
×
833
                return addStandardContext(mav);
×
834
        }
835

836
        @Override
837
        @Action("retagging a machine")
838
        public ModelAndView retagMachine(String machineName, String newTags) {
839
                var tags =
×
840
                                stream(newTags.split(",")).map(String::strip).collect(toSet());
×
841
                machineController.updateTags(machineName, tags);
×
842
                log.info("retagged {} to have tags {}", machineName, tags);
×
843
                return machineManagement(NULL_MAP);
×
844
        }
845

846
        @Override
847
        @Action("disabling a machine")
848
        public ModelAndView disableMachine(String machineName) {
849
                machineController.setMachineState(machineName, false);
×
850
                log.info("marked {} as out of service", machineName);
×
851
                return machineManagement(NULL_MAP);
×
852
        }
853

854
        @Override
855
        @Action("enabling a machine")
856
        public ModelAndView enableMachine(String machineName) {
857
                machineController.setMachineState(machineName, true);
×
858
                log.info("marked {} as in service", machineName);
×
859
                return machineManagement(NULL_MAP);
×
860
        }
861

862
        @Override
863
        @Action("defining a machine")
864
        public ModelAndView defineMachine(MultipartFile file) {
865
                var machines = extractMachineDefinitions(file);
×
866
                for (var m : machines) {
×
867
                        machineDefiner.loadMachineDefinition(m);
×
868
                        log.info("defined machine {}", m.getName());
×
869
                }
×
870
                var mav = machineManagement(NULL_MAP);
×
871
                // Tailor with extra objects here
872
                mav.addObject(DEFINED_MACHINES_OBJ, machines);
×
873
                return mav;
×
874
        }
875

876
        private List<Machine> extractMachineDefinitions(MultipartFile file) {
877
                try (var input = buffer(file.getInputStream())) {
×
878
                        return machineDefiner.readMachineDefinitions(input);
×
879
                } catch (IOException e) {
×
880
                        throw new AdminException(
×
881
                                        "problem with processing file: " + e.getMessage());
×
882
                }
883
        }
884
}
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