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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

19 Sep 2023 08:46AM UTC coverage: 36.409% (-0.6%) from 36.982%
6233274834

Pull #658

github

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

1656 of 1656 new or added lines in 260 files covered. (100.0%)

8373 of 22997 relevant lines covered (36.41%)

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_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.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 javax.ws.rs.WebApplicationException;
92
import javax.ws.rs.core.Response.Status;
93

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

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

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

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

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

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

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

152
        @Autowired
153
        private UserControl userManager;
154

155
        @Autowired
156
        private MachineStateControl machineController;
157

158
        @Autowired
159
        private MachineDefinitionLoader machineDefiner;
160

161
        @Autowired
162
        private SpallocAPI spalloc;
163

164
        @Autowired
165
        private QuotaManager quotaManager;
166

167
        @Autowired
168
        private URLPathMaker urlMaker;
169

170
        private class MachineName {
171
                String name;
172

173
                boolean inService;
174

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

391
        // The actual controller methods
392

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

699
                inflateBoardRecord(board, bs);
×
700

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

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

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

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

751
                return data;
×
752
        }
753

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc