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

SpiNNakerManchester / JavaSpiNNaker / 14529798188

21 Mar 2025 04:17PM UTC coverage: 38.266% (-0.3%) from 38.579%
14529798188

push

github

web-flow
Merge pull request #1222 from SpiNNakerManchester/more_spalloc_rest_calls

More spalloc rest calls

70 of 815 new or added lines in 33 files covered. (8.59%)

29 existing lines in 13 files now uncovered.

9193 of 24024 relevant lines covered (38.27%)

1.15 hits per line

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

6.21
/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.transceiver.TransceiverInterface;
87
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
88

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

131
        private static final long WAIT_FOR_POWER_SECONDS = 60;
132

133
        private static final int N_CORES = 18;
134

135
        @Autowired
136
        private SpallocAPI spallocCore;
137

138
        @Autowired
139
        private LogoutHandler logoutHandler;
140

141
        @Autowired
142
        private UserControl userManager;
143

144
        @Autowired
145
        private URLPathMaker urlMaker;
146

147
        @Autowired
148
        private ServiceVersion version;
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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