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

SpiNNakerManchester / JavaSpiNNaker / 6187484699

14 Sep 2023 03:05PM UTC coverage: 36.874% (-0.006%) from 36.88%
6187484699

push

github

web-flow
Merge pull request #1054 from SpiNNakerManchester/blacklist_ui

Blacklist UI

61 of 61 new or added lines in 8 files covered. (100.0%)

8658 of 23480 relevant lines covered (36.87%)

0.37 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_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

90
import javax.ws.rs.WebApplicationException;
91
import javax.ws.rs.core.Response.Status;
92

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

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
3✔
136
                implements AdminController {
137
        private static final Logger log = getLogger(AdminControllerImpl.class);
3✔
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;
3✔
144

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

148
        /** Dummy attributes. */
149
        private static final RedirectAttributes NULL_ATTR = null;
3✔
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
                private static final long serialVersionUID = 8401068773689159840L;
334

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

340
        private static final class NoUser extends AdminException {
341
                private static final long serialVersionUID = 6430674580385445089L;
342

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

348
        private static final class NoGroup extends AdminException {
349
                private static final long serialVersionUID = -4593707687103047377L;
350

351
                private NoGroup() {
352
                        super("no such group");
×
353
                }
×
354
        }
355

356
        private static final class NoBoard extends AdminException {
357
                private static final long serialVersionUID = -4017368969526085002L;
358

359
                private NoBoard() {
360
                        super("no such board");
×
361
                }
×
362
        }
363

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

386
        // The actual controller methods
387

388
        @Override
389
        @Action("getting the main admin UI")
390
        public ModelAndView mainUI(ModelMap ignored) {
391
                return addStandardContext(MAIN_VIEW.view());
×
392
        }
393

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

694
                inflateBoardRecord(board, bs);
×
695

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

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

713
        @Override
714
        @Action("saving changes to a board blacklist")
715
        public ResponseEntity<Void> blacklistSave(BlacklistData bldata) {
716

717
                if (bldata.isPresent()) {
×
718
                        try {
719
                                var blacklist = bldata.getParsedBlacklist();
×
720
                                machineController.writeBlacklistToDB(bldata.getBoardId(),
×
721
                                                blacklist);
722
                                machineController.writeBlacklistToMachine(bldata.getBoardId(),
×
723
                                                bldata.getBmpId(), blacklist);
×
724
                        } catch (InterruptedException e) {
×
725
                                throw new WebApplicationException(e);
×
726
                        }
×
727
                }
728
                return new ResponseEntity<>(HttpStatus.OK);
×
729
        }
730

731
        @Override
732
        @Action("fetching a live board blacklist from the machine")
733
        public BlacklistData blacklistFetch(int boardId, int bmpId) {
734

735
                log.info("pulling blacklist from board {}", boardId);
×
736
                BlacklistData data = new BlacklistData();
×
737
                data.setBoardId(boardId);
×
738
                data.setBmpId(bmpId);
×
739
                machineController.pullBlacklist(boardId, bmpId).ifPresentOrElse(bl -> {
×
740
                        data.setPresent(true);
×
741
                        data.setSynched(true);
×
742
                        data.setBlacklist(bl.render());
×
743
                }, () -> {
×
744
                        data.setPresent(false);
×
745
                        data.setSynched(false);
×
746
                });
×
747

748
                return data;
×
749
        }
750

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

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

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

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

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

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

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

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

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

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

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