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

SpiNNakerManchester / JavaSpiNNaker / 13178228403

06 Feb 2025 11:57AM UTC coverage: 38.596% (-0.03%) from 38.623%
13178228403

push

github

rowleya
Try to fix the table

0 of 1 new or added line in 1 file covered. (0.0%)

7 existing lines in 2 files now uncovered.

9184 of 23795 relevant lines covered (38.6%)

1.16 hits per line

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

6.57
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/web/SystemControllerImpl.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.web;
17

18
import static java.nio.charset.StandardCharsets.UTF_8;
19
import static java.util.Objects.nonNull;
20
import static org.slf4j.LoggerFactory.getLogger;
21
import static org.springframework.http.HttpStatus.NOT_FOUND;
22
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
23
import static org.springframework.security.core.context.SecurityContextHolder.getContext;
24
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
25
import static org.springframework.web.servlet.support.ServletUriComponentsBuilder.fromCurrentRequestUri;
26
import static uk.ac.manchester.spinnaker.alloc.security.SecurityConfig.IS_READER;
27
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.error;
28
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.errorMessage;
29
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.uri;
30
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.MAIN_URI;
31
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_URI;
32
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_URI;
33
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_URI;
34
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_OIDC_URI;
35
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_URI;
36
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_URI;
37
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_PATH;
38
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_OIDC_PATH;
39
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_PATH;
40
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_PATH;
41
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_PATH;
42
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_PATH;
43

44
import java.security.Principal;
45
import java.time.Duration;
46
import java.util.ArrayList;
47
import java.util.List;
48
import java.util.Map;
49

50
import javax.servlet.http.HttpServletRequest;
51
import javax.servlet.http.HttpServletResponse;
52
import javax.validation.Valid;
53

54
import org.slf4j.Logger;
55
import org.springframework.beans.factory.annotation.Autowired;
56
import org.springframework.dao.DataAccessException;
57
import org.springframework.security.access.prepost.PreAuthorize;
58
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
59
import org.springframework.security.core.AuthenticationException;
60
import org.springframework.security.web.authentication.logout.LogoutHandler;
61
import org.springframework.stereotype.Controller;
62
import org.springframework.ui.ModelMap;
63
import org.springframework.validation.BindException;
64
import org.springframework.web.bind.annotation.ExceptionHandler;
65
import org.springframework.web.bind.annotation.GetMapping;
66
import org.springframework.web.method.HandlerMethod;
67
import org.springframework.web.server.ResponseStatusException;
68
import org.springframework.web.servlet.ModelAndView;
69
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
70

71
import com.google.errorprone.annotations.Keep;
72

73
import uk.ac.manchester.spinnaker.alloc.ServiceConfig.URLPathMaker;
74
import uk.ac.manchester.spinnaker.alloc.ServiceVersion;
75
import uk.ac.manchester.spinnaker.alloc.admin.UserControl;
76
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI;
77
import uk.ac.manchester.spinnaker.alloc.model.PasswordChangeRecord;
78
import uk.ac.manchester.spinnaker.alloc.security.AppAuthTransformationFilter;
79
import uk.ac.manchester.spinnaker.alloc.security.Permit;
80
import uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.ViewFactory;
81
import uk.ac.manchester.spinnaker.machine.CoreSubsets;
82
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
83

84
/**
85
 * The main web interface controller.
86
 *
87
 * @author Donal Fellows
88
 */
89
@Controller("mvc.mainUI")
90
public class SystemControllerImpl implements SystemController {
3✔
91
        private static final Logger log = getLogger(SystemControllerImpl.class);
3✔
92

93
        private static final ViewFactory MAIN_VIEW = new ViewFactory("index");
3✔
94

95
        private static final ViewFactory LOGIN_VIEW = new ViewFactory("login");
3✔
96

97
        private static final ViewFactory PASSWORD_CHANGE_VIEW =
3✔
98
                        new ViewFactory("password");
99

100
        private static final ViewFactory MACHINE_LIST_VIEW =
3✔
101
                        new ViewFactory("listmachines");
102

103
        private static final ViewFactory MACHINE_VIEW =
3✔
104
                        new ViewFactory("machinedetails");
105

106
        private static final ViewFactory JOB_LIST_VIEW =
3✔
107
                        new ViewFactory("listjobs");
108

109
        private static final ViewFactory JOB_VIEW = new ViewFactory("jobdetails");
3✔
110

111
        // Must match what views expect
112
        private static final String VERSION_OBJ = "version";
113

114
        private static final String BUILD_OBJ = "build";
115

116
        private static final String JOBS_OBJ = "jobList";
117

118
        private static final String ONE_JOB_OBJ = "job";
119

120
        private static final String MACHINES_OBJ = "machineList";
121

122
        private static final String ONE_MACHINE_OBJ = "machine";
123

124
        private static final String BASE_URI = "baseuri";
125

126
        private static final long WAIT_FOR_POWER_SECONDS = 60;
127

128
        private static final int N_CORES = 18;
129

130
        @Autowired
131
        private SpallocAPI spallocCore;
132

133
        @Autowired
134
        private LogoutHandler logoutHandler;
135

136
        @Autowired
137
        private UserControl userManager;
138

139
        @Autowired
140
        private URLPathMaker urlMaker;
141

142
        @Autowired
143
        private ServiceVersion version;
144

145
        /**
146
         * All models should contain a common set of attributes that describe where
147
         * the view is rendering and where other parts of the admin interface are.
148
         * Only call from
149
         * {@link #addStandardContext(ModelAndView, RedirectAttributes)}.
150
         *
151
         * @param model
152
         *            The base model to add to. This may be the real model or the
153
         *            flash attributes.
154
         */
155
        private void addStandardContextAttrs(Map<String, Object> model) {
156
                var auth = getContext().getAuthentication();
×
157
                boolean mayChangePassword =
×
158
                                auth instanceof UsernamePasswordAuthenticationToken;
159

160
                model.put(BASE_URI, fromCurrentRequestUri().toUriString());
×
161
                model.put(MAIN_URI, uri(self().index()));
×
162
                model.put(CHANGE_PASSWORD_URI, urlMaker.systemUrl(
×
163
                                CHANGE_PASSWORD_PATH));
164
                model.put(LOGOUT_URI, urlMaker.systemUrl(LOGOUT_PATH));
×
165
                model.put(SPALLOC_CSS_URI, urlMaker.systemUrl(SPALLOC_CSS_PATH));
×
166
                model.put(SPALLOC_JS_URI, urlMaker.systemUrl(SPALLOC_JS_PATH));
×
167
                model.put(USER_MAY_CHANGE_PASSWORD, mayChangePassword);
×
168
        }
×
169

170
        /**
171
         * All models should contain a common set of attributes that describe where
172
         * the view is rendering and where other parts of the admin interface are.
173
         *
174
         * @param mav
175
         *            The model-and-view.
176
         * @param attrs
177
         *            The redirect attributes, or {@code null} if this is not a
178
         *            redirect.
179
         * @return The enhanced model-and-view.
180
         */
181
        private ModelAndView addStandardContext(ModelAndView mav,
182
                        RedirectAttributes attrs) {
183
                addStandardContextAttrs(nonNull(attrs)
×
184
                                // Real implementation of flash attrs is always a ModelMap
185
                                ? (ModelMap) attrs.getFlashAttributes()
×
186
                                : mav.getModel());
×
187
                return mav;
×
188
        }
189

190
        /**
191
         * All models should contain a common set of attributes that describe where
192
         * the view is rendering and where other parts of the admin interface are.
193
         *
194
         * @param mav
195
         *            The model-and-view.
196
         * @return The enhanced model-and-view.
197
         */
198
        private ModelAndView addStandardContext(ModelAndView mav) {
199
                return addStandardContext(mav, null);
×
200
        }
201

202
        private ModelAndView view(ViewFactory name, String key, Object value) {
203
                var mav = name.view();
×
204
                mav.addObject(key, value);
×
205
                return addStandardContext(mav);
×
206
        }
207

208
        @Override
209
        @GetMapping("/")
210
        public ModelAndView index() {
211
                return addStandardContext(MAIN_VIEW.view())
×
212
                                .addObject(VERSION_OBJ, version.getFullVersion())
×
213
                                .addObject(BUILD_OBJ, version.getBuildTimestamp());
×
214
        }
215

216
        @Override
217
        @GetMapping("/login.html")
218
        public ModelAndView login() {
219
                var mav = LOGIN_VIEW.view();
×
220
                var model = mav.getModel();
×
221
                model.put(LOGIN_URI, urlMaker.systemUrl(LOGIN_PATH));
×
222
                model.put(LOGIN_OIDC_URI, urlMaker.systemUrl(LOGIN_OIDC_PATH));
×
223
                return mav;
×
224
        }
225

226
        @Override
227
        @GetMapping("/change_password")
228
        @Action("preparing for password change")
229
        public ModelAndView getPasswordChangeForm(Principal principal) {
230
                return view(PASSWORD_CHANGE_VIEW, USER_PASSWORD_CHANGE_ATTR,
×
231
                                userManager.getUser(principal));
×
232
        }
233

234
        /**
235
         * Handle {@linkplain Valid invalid} arguments.
236
         *
237
         * @param result
238
         *            The result of validation.
239
         * @return How to render this to the user.
240
         */
241
        @Keep
242
        @ExceptionHandler(BindException.class)
243
        @UsedInJavadocOnly(Valid.class)
244
        private ModelAndView bindingError(BindException result) {
245
                if (result.hasGlobalErrors()) {
×
246
                        log.debug("binding problem", result);
×
247
                        return error(errorMessage(result.getGlobalError()));
×
248
                } else if (result.hasFieldErrors()) {
×
249
                        log.debug("binding problem", result);
×
250
                        return error(errorMessage(result.getFieldError()));
×
251
                } else {
252
                        log.error("unknown binding error", result);
×
253
                        return error("unknown error");
×
254
                }
255
        }
256

257
        /**
258
         * Handle database and auth exceptions flowing out through this controller.
259
         *
260
         * @param e
261
         *            The exception that happened.
262
         * @param hm
263
         *            What was happening to cause a problem.
264
         * @return View to describe what's going on to the user.
265
         */
266
        @Keep
267
        @ExceptionHandler({
268
                AuthenticationException.class, DataAccessException.class
269
        })
270
        private ModelAndView dbError(RuntimeException e, HandlerMethod hm) {
271
                var a = hm.getMethodAnnotation(Action.class);
×
272
                String message;
273
                if (e instanceof AuthenticationException) {
×
274
                        message = "authentication problem";
×
275
                        if (nonNull(a)) {
×
276
                                log.error("auth problem when {}", a.value(), e);
×
277
                        } else {
278
                                log.error("general authentication problem", e);
×
279
                        }
280
                } else if (e instanceof DataAccessException) {
×
281
                        message = "database problem";
×
282
                        if (nonNull(a)) {
×
283
                                log.error("database problem when {}", a.value(), e);
×
284
                        } else {
285
                                log.error("general database problem", e);
×
286
                        }
287
                } else {
288
                        message = "general problem";
×
289
                        log.error("general problem", e);
×
290
                }
291
                if (new Permit(getContext()).admin) {
×
292
                        return error(message + ": " + e.getMessage());
×
293
                } else {
294
                        return error(message);
×
295
                }
296
        }
297

298
        @Override
299
        @Action(CHANGE_PASSWORD_PATH)
300
        public ModelAndView postPasswordChangeForm(
301
                        PasswordChangeRecord user,
302
                        Principal principal) {
303
                log.info("changing password for {}", principal.getName());
×
304
                return view(PASSWORD_CHANGE_VIEW, USER_PASSWORD_CHANGE_ATTR,
×
305
                                userManager.updateUser(principal, user));
×
306
        }
307

308
        @Override
309
        @Action("logging out")
310
        public String performLogout(HttpServletRequest request,
311
                        HttpServletResponse response) {
312
                var auth = getContext().getAuthentication();
×
313
                AppAuthTransformationFilter.clearToken(request);
×
314
                if (nonNull(auth)) {
×
315
                        log.info("logging out {}", auth.getPrincipal());
×
316
                        logoutHandler.logout(request, response, auth);
×
317
                }
318
                return "redirect:" + urlMaker.systemUrl("login.html");
×
319
        }
320

321
        private static SystemController self() {
322
                // Do not refactor to a constant; request context aware!
323
                return on(SystemController.class);
×
324
        }
325

326
        @Override
327
        @PreAuthorize(IS_READER)
328
        @Action("listing machines")
329
        public ModelAndView getMachineList() {
330
                var table = spallocCore.listMachines(false);
×
331
                table.forEach(rec -> rec
×
332
                                .setDetailsUrl(uri(self().getMachineInfo(rec.getName()))));
×
333
                return view(MACHINE_LIST_VIEW, MACHINES_OBJ, table);
×
334
        }
335

336
        @Override
337
        @PreAuthorize(IS_READER)
338
        @Action("getting machine details")
339
        public ModelAndView getMachineInfo(String machine) {
340
                var permit = new Permit(getContext());
×
341
                /*
342
                 * Admins can get the view for disabled machines, but if they know it is
343
                 * there.
344
                 */
345
                var mach = spallocCore
×
346
                                .getMachineInfo(machine, permit.admin, permit)
×
347
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
348
                // Owners and admins may drill down further into jobs
349
                mach.getJobs().stream().filter(j -> j.getOwner().isPresent())
×
350
                                .forEach(j -> j.setUrl(uri(self().getJobInfo(j.getId()))));
×
351
                return view(MACHINE_VIEW, ONE_MACHINE_OBJ, mach);
×
352
        }
353

354
        @Override
355
        @PreAuthorize(IS_READER)
356
        @Action("listing jobs")
357
        public ModelAndView getJobList() {
358
                var table = spallocCore.listJobs(new Permit(getContext()));
×
359
                table.forEach(entry -> {
×
360
                        entry.setDetailsUrl(uri(self().getJobInfo(entry.getId())));
×
361
                        entry.setMachineUrl(
×
362
                                        uri(self().getMachineInfo(entry.getMachineName())));
×
363
                });
×
364
                return view(JOB_LIST_VIEW, JOBS_OBJ, table);
×
365
        }
366

367
        @Override
368
        @PreAuthorize(IS_READER)
369
        @Action("getting job details")
370
        public ModelAndView getJobInfo(int id) {
371
                var permit = new Permit(getContext());
×
372
                var job = spallocCore.getJobInfo(permit, id)
×
373
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
374
                if (nonNull(job.getRequestBytes())) {
×
375
                        job.setRequest(new String(job.getRequestBytes(), UTF_8));
×
376
                }
377
                job.setMachineUrl(uri(self().getMachineInfo(job.getMachine())));
×
378
                var mav = view(JOB_VIEW, ONE_JOB_OBJ, job);
×
379
                mav.addObject("deleteUri", uri(self().destroyJob(id, null)));
×
380
                mav.addObject("powerUri", uri(self().powerJob(id, false)));
×
381
                mav.addObject("processUri", uri(self().listProcesses(id, 0, 0)));
×
382
                return mav;
×
383
        }
384

385
        @Override
386
        @PreAuthorize(IS_READER)
387
        @Action("deleting job")
388
        public ModelAndView destroyJob(int id, String reason) {
389
                var permit = new Permit(getContext());
×
390
                var job = spallocCore.getJob(permit, id)
×
391
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
392
                job.destroy(reason);
×
393
                var mach = spallocCore.getJobInfo(permit, id)
×
394
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
395
                if (nonNull(mach.getRequestBytes())) {
×
396
                        mach.setRequest(new String(mach.getRequestBytes(), UTF_8));
×
397
                }
398
                mach.setMachineUrl(uri(self().getMachineInfo(mach.getMachine())));
×
399
                return view(JOB_VIEW, ONE_JOB_OBJ, mach);
×
400
        }
401

402
        @Override
403
        @PreAuthorize(IS_READER)
404
        @Action("power job")
405
        public ModelAndView powerJob(int id, boolean power) {
406
                var permit = new Permit(getContext());
×
407
                var job = spallocCore.getJob(permit, id)
×
408
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
409
                job.setPower(power);
×
410
                job.waitForChange(Duration.ofSeconds(WAIT_FOR_POWER_SECONDS));
×
411
                var mach = spallocCore.getJobInfo(permit, id)
×
412
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
413
                if (nonNull(mach.getRequestBytes())) {
×
414
                        mach.setRequest(new String(mach.getRequestBytes(), UTF_8));
×
415
                }
416
                mach.setMachineUrl(uri(self().getMachineInfo(mach.getMachine())));
×
417
                return view(JOB_VIEW, ONE_JOB_OBJ, mach);
×
418
        }
419

420
        @Override
421
        @PreAuthorize(IS_READER)
422
        @Action("getting job process listing")
423
        public List<Process> listProcesses(int id, int x, int y) {
424
                var permit = new Permit(getContext());
×
425
                var job = spallocCore.getJob(permit, id)
×
426
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
427
                try {
428
                        var txrx = job.getMachine().get().getTransceiver();
×
429
                        CoreSubsets cores = new CoreSubsets();
×
430
                        for (int i = 0; i < N_CORES; i++) {
×
431
                                cores.addCore(x, y, i);
×
432
                        }
433
                        var info = txrx.getCPUInformation(cores);
×
434
                        var response = new ArrayList<Process>();
×
435
                        for (var inf : info) {
×
436
                                response.add(new Process(inf));
×
437
                        }
×
438
                        return response;
×
439
                } catch (Exception e) {
×
NEW
440
                        log.error("Error receiving process details", e);
×
UNCOV
441
                        throw new ResponseStatusException(INTERNAL_SERVER_ERROR,
×
442
                                        "Error receiving process details", e);
443
                }
444
        }
445

446
}
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