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

SpiNNakerManchester / JavaSpiNNaker / 13179150406

06 Feb 2025 12:49PM UTC coverage: 38.585% (-0.03%) from 38.613%
13179150406

push

github

rowleya
Fix the object too

9184 of 23802 relevant lines covered (38.58%)

1.15 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.security.core.context.SecurityContextHolder.getContext;
23
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
24
import static org.springframework.web.servlet.support.ServletUriComponentsBuilder.fromCurrentRequestUri;
25
import static uk.ac.manchester.spinnaker.alloc.security.SecurityConfig.IS_READER;
26
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.error;
27
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.errorMessage;
28
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.uri;
29
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.MAIN_URI;
30
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_URI;
31
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_URI;
32
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_URI;
33
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_OIDC_URI;
34
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_URI;
35
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_URI;
36
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_PATH;
37
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGIN_OIDC_PATH;
38
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.CHANGE_PASSWORD_PATH;
39
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.LOGOUT_PATH;
40
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_CSS_PATH;
41
import static uk.ac.manchester.spinnaker.alloc.web.ControllerUtils.SPALLOC_JS_PATH;
42

43
import java.net.SocketTimeoutException;
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
import javax.ws.rs.core.Response.Status;
54

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

129
        private static final long WAIT_FOR_POWER_SECONDS = 60;
130

131
        private static final int N_CORES = 18;
132

133
        @Autowired
134
        private SpallocAPI spallocCore;
135

136
        @Autowired
137
        private LogoutHandler logoutHandler;
138

139
        @Autowired
140
        private UserControl userManager;
141

142
        @Autowired
143
        private URLPathMaker urlMaker;
144

145
        @Autowired
146
        private ServiceVersion version;
147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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