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

SpiNNakerManchester / JavaSpiNNaker / 13178749274

06 Feb 2025 12:26PM UTC coverage: 38.613% (+0.02%) from 38.596%
13178749274

push

github

rowleya
Fix started rather than duration

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

77 existing lines in 1 file now uncovered.

9190 of 23800 relevant lines covered (38.61%)

1.16 hits per line

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

6.25
/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.http.HttpStatus.GATEWAY_TIMEOUT;
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.security.SecurityConfig.IS_READER;
28
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.error;
29
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.errorMessage;
30
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.uri;
31
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.MAIN_URI;
32
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_URI;
33
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_URI;
34
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_URI;
35
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_OIDC_URI;
36
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_URI;
37
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_URI;
38
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_PATH;
39
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_OIDC_PATH;
40
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_PATH;
41
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_PATH;
42
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_PATH;
43
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_PATH;
44

45
import java.net.SocketTimeoutException;
46
import java.security.Principal;
47
import java.time.Duration;
48
import java.util.ArrayList;
49
import java.util.List;
50
import java.util.Map;
51

52
import javax.servlet.http.HttpServletRequest;
53
import javax.servlet.http.HttpServletResponse;
54
import javax.validation.Valid;
55

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

73
import com.google.errorprone.annotations.Keep;
74

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

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

97
        private static final ViewFactory MAIN_VIEW = new ViewFactory("index");
3✔
98

99
        private static final ViewFactory LOGIN_VIEW = new ViewFactory("login");
3✔
100

101
        private static final ViewFactory PASSWORD_CHANGE_VIEW =
3✔
102
                        new ViewFactory("password");
103

104
        private static final ViewFactory MACHINE_LIST_VIEW =
3✔
105
                        new ViewFactory("listmachines");
106

107
        private static final ViewFactory MACHINE_VIEW =
3✔
108
                        new ViewFactory("machinedetails");
109

110
        private static final ViewFactory JOB_LIST_VIEW =
3✔
111
                        new ViewFactory("listjobs");
112

113
        private static final ViewFactory JOB_VIEW = new ViewFactory("jobdetails");
3✔
114

115
        // Must match what views expect
116
        private static final String VERSION_OBJ = "version";
117

118
        private static final String BUILD_OBJ = "build";
119

120
        private static final String JOBS_OBJ = "jobList";
121

122
        private static final String ONE_JOB_OBJ = "job";
123

124
        private static final String MACHINES_OBJ = "machineList";
125

126
        private static final String ONE_MACHINE_OBJ = "machine";
127

128
        private static final String BASE_URI = "baseuri";
129

130
        private static final long WAIT_FOR_POWER_SECONDS = 60;
131

132
        private static final int N_CORES = 18;
133

134
        @Autowired
135
        private SpallocAPI spallocCore;
136

137
        @Autowired
138
        private LogoutHandler logoutHandler;
139

140
        @Autowired
141
        private UserControl userManager;
142

143
        @Autowired
144
        private URLPathMaker urlMaker;
145

146
        @Autowired
147
        private ServiceVersion version;
148

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

164
                model.put(BASE_URI, fromCurrentRequestUri().toUriString());
×
165
                model.put(MAIN_URI, uri(self().index()));
×
166
                model.put(CHANGE_PASSWORD_URI, urlMaker.systemUrl(
×
167
                                CHANGE_PASSWORD_PATH));
168
                model.put(LOGOUT_URI, urlMaker.systemUrl(LOGOUT_PATH));
×
UNCOV
169
                model.put(SPALLOC_CSS_URI, urlMaker.systemUrl(SPALLOC_CSS_PATH));
×
UNCOV
170
                model.put(SPALLOC_JS_URI, urlMaker.systemUrl(SPALLOC_JS_PATH));
×
UNCOV
171
                model.put(USER_MAY_CHANGE_PASSWORD, mayChangePassword);
×
UNCOV
172
        }
×
173

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

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

206
        private ModelAndView view(ViewFactory name, String key, Object value) {
UNCOV
207
                var mav = name.view();
×
UNCOV
208
                mav.addObject(key, value);
×
UNCOV
209
                return addStandardContext(mav);
×
210
        }
211

212
        @Override
213
        @GetMapping("/")
214
        public ModelAndView index() {
UNCOV
215
                return addStandardContext(MAIN_VIEW.view())
×
UNCOV
216
                                .addObject(VERSION_OBJ, version.getFullVersion())
×
UNCOV
217
                                .addObject(BUILD_OBJ, version.getBuildTimestamp());
×
218
        }
219

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

230
        @Override
231
        @GetMapping("/change_password")
232
        @Action("preparing for password change")
233
        public ModelAndView getPasswordChangeForm(Principal principal) {
UNCOV
234
                return view(PASSWORD_CHANGE_VIEW, USER_PASSWORD_CHANGE_ATTR,
×
UNCOV
235
                                userManager.getUser(principal));
×
236
        }
237

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

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

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

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

325
        private static SystemController self() {
326
                // Do not refactor to a constant; request context aware!
UNCOV
327
                return on(SystemController.class);
×
328
        }
329

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

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

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

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

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

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

424
        @Override
425
        @PreAuthorize(IS_READER)
426
        @Action("getting job process listing")
427
        public List<Process> listProcesses(int id, int x, int y) {
428
                var permit = new Permit(getContext());
×
429
                var job = spallocCore.getJob(permit, id)
×
430
                                .orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
×
431
                try {
UNCOV
432
                        var txrx = job.getMachine().get().getTransceiver();
×
433
                        CoreSubsets cores = new CoreSubsets();
×
434
                        for (int i = 0; i < N_CORES; i++) {
×
435
                                cores.addCore(x, y, i);
×
436
                        }
437
                        var info = txrx.getCPUInformation(cores);
×
438
                        var response = new ArrayList<Process>();
×
439
                        for (var inf : info) {
×
440
                                response.add(new Process(inf));
×
441
                        }
×
UNCOV
442
                        return response;
×
UNCOV
443
                } catch (NoP2PRoute e) {
×
UNCOV
444
                        throw new ResponseStatusException(NOT_FOUND, e.getMessage(), e);
×
UNCOV
445
                } catch (ProcessException e) {
×
UNCOV
446
                        var cause = e.getCause();
×
UNCOV
447
                        if (cause instanceof SocketTimeoutException) {
×
UNCOV
448
                                throw new ResponseStatusException(GATEWAY_TIMEOUT,
×
449
                                                "Timeout waiting for process information",
450
                                                cause);
451
                        } else {
UNCOV
452
                                throw new ResponseStatusException(INTERNAL_SERVER_ERROR,
×
453
                                                "Error receiving process details", e);
454
                        }
UNCOV
455
                } catch (Exception e) {
×
UNCOV
456
                        log.error("Error receiving process details", e);
×
UNCOV
457
                        throw new ResponseStatusException(INTERNAL_SERVER_ERROR,
×
458
                                        "Error receiving process details", e);
459
                }
460
        }
461

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