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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

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

Pull #658

github

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

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

8373 of 22997 relevant lines covered (36.41%)

0.36 hits per line

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

1.49
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/web/SpallocServiceImpl.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.lang.Math.max;
19
import static java.lang.String.format;
20
import static java.lang.String.join;
21
import static java.util.Objects.isNull;
22
import static java.util.Objects.nonNull;
23
import static java.util.stream.Collectors.toList;
24
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
25
import static javax.ws.rs.core.Response.created;
26
import static javax.ws.rs.core.Response.ok;
27
import static javax.ws.rs.core.Response.status;
28
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
29
import static org.slf4j.LoggerFactory.getLogger;
30
import static uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateBoard.address;
31
import static uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateBoard.physical;
32
import static uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateBoard.triad;
33
import static uk.ac.manchester.spinnaker.alloc.web.WebServiceComponentNames.SERV;
34
import static uk.ac.manchester.spinnaker.utils.OptionalUtils.ifElse;
35

36
import java.net.URI;
37
import java.util.ArrayList;
38
import java.util.HashMap;
39
import java.util.Optional;
40

41
import javax.servlet.http.HttpServletRequest;
42
import javax.ws.rs.Path;
43
import javax.ws.rs.container.AsyncResponse;
44
import javax.ws.rs.core.Response;
45
import javax.ws.rs.core.SecurityContext;
46
import javax.ws.rs.core.UriInfo;
47

48
import org.slf4j.Logger;
49
import org.springframework.beans.factory.ObjectProvider;
50
import org.springframework.beans.factory.annotation.Autowired;
51
import org.springframework.security.web.csrf.CsrfToken;
52
import org.springframework.stereotype.Service;
53

54
import com.fasterxml.jackson.core.JsonProcessingException;
55
import com.fasterxml.jackson.databind.json.JsonMapper;
56

57
import uk.ac.manchester.spinnaker.alloc.ServiceVersion;
58
import uk.ac.manchester.spinnaker.alloc.SpallocProperties;
59
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI;
60
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateDescriptor;
61
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateDimensions;
62
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateDimensionsAt;
63
import uk.ac.manchester.spinnaker.alloc.allocator.SpallocAPI.CreateNumBoards;
64
import uk.ac.manchester.spinnaker.alloc.security.Permit;
65
import uk.ac.manchester.spinnaker.alloc.web.RequestFailedException.BadArgs;
66
import uk.ac.manchester.spinnaker.alloc.web.RequestFailedException.NotFound;
67

68
/**
69
 * The implementation of the user-facing REST API. Operations are delegated to
70
 * {@link SpallocAPI} for fulfilment; this class is responsible for turning the
71
 * operations described by users into the form understood by the service core,
72
 * and for converting the responses. It also handles the transfer of calls onto
73
 * suitable worker threads, where appropriate.
74
 *
75
 * @author Donal Fellows
76
 */
77
@Service("service")
78
@Path(SERV)
79
public class SpallocServiceImpl extends BackgroundSupport
1✔
80
                implements SpallocServiceAPI {
81
        private static final Logger log = getLogger(SpallocServiceImpl.class);
1✔
82

83
        @Autowired
84
        private ServiceVersion version;
85

86
        @Autowired
87
        private SpallocProperties properties;
88

89
        @Autowired
90
        private SpallocAPI core;
91

92
        @Autowired
93
        private JsonMapper mapper;
94

95
        /**
96
         * Factory for {@linkplain MachineAPI machines}. Only use via
97
         * {@link #getMachine(String, UriInfo, SecurityContext) getMachine(...)};
98
         * this is because we're dealing with prototype beans.
99
         */
100
        @Autowired
101
        private ObjectProvider<MachineAPI> machineFactory;
102

103
        /**
104
         * Factory for {@linkplain JobAPI jobs}. Only use via
105
         * {@link #getJob(int, UriInfo, HttpServletRequest, SecurityContext)
106
         * getJob(...)}; this is because we're dealing with prototype beans.
107
         */
108
        @Autowired
109
        private ObjectProvider<JobAPI> jobFactory;
110

111
        @Override
112
        public ServiceDescription describeService(UriInfo ui, SecurityContext sec,
113
                        HttpServletRequest req) {
114
                var token = (CsrfToken) req.getAttribute("_csrf");
×
115
                return new ServiceDescription(version.getVersion(), ui, sec, token);
×
116
        }
117

118
        @Override
119
        public MachinesResponse getMachines(UriInfo ui) {
120
                return new MachinesResponse(core.getMachines(false), ui);
×
121
        }
122

123
        @Override
124
        public MachineAPI getMachine(String name, UriInfo ui, SecurityContext sec) {
125
                var permit = new Permit(sec);
×
126
                var machine = core.getMachine(name, permit.admin)
×
127
                                .orElseThrow(() -> new NotFound("no such machine"));
×
128
                // Wrap so we can use security annotations
129
                return machineFactory.getObject(machine, ui);
×
130
        }
131

132
        @Override
133
        public JobAPI getJob(int id, UriInfo ui, HttpServletRequest req,
134
                        SecurityContext security) {
135
                var permit = new Permit(security);
×
136
                var j = core.getJob(permit, id)
×
137
                                .orElseThrow(() -> new NotFound("no such job"));
×
138
                // Wrap so we can use security annotations
139
                return jobFactory.getObject(j, req.getRemoteHost(), permit, ui);
×
140
        }
141

142
        // Could be configurable, but no real point
143
        private static final int LIMIT_LIMIT = 200;
144

145
        /**
146
         * Adds in the {@code Link:} header with general paging info.
147
         *
148
         * @param value
149
         *            The core response.
150
         * @param ui
151
         *            Information about URIs
152
         * @param start
153
         *            The start offset.
154
         * @param limit
155
         *            The size of chunk.
156
         * @return Annotated response.
157
         */
158
        private Response wrapPaging(ListJobsResponse value, UriInfo ui, int start,
159
                        int limit) {
160
                var r = ok(value);
×
161
                var links = new HashMap<String, URI>();
×
162
                if (start > 0) {
×
163
                        var prev = ui.getRequestUriBuilder()
×
164
                                        .replaceQueryParam("wait", false)
×
165
                                        .replaceQueryParam("start", max(start - limit, 0)).build();
×
166
                        value.setPrev(prev);
×
167
                        links.put("prev", prev);
×
168
                }
169
                if (value.jobs.size() == limit) {
×
170
                        var next = ui.getRequestUriBuilder()
×
171
                                        .replaceQueryParam("wait", false)
×
172
                                        .replaceQueryParam("start", start + limit).build();
×
173
                        value.setNext(next);
×
174
                        links.put("next", next);
×
175
                }
176
                if (!links.isEmpty()) {
×
177
                        r.header("Link", join(", ", links.entrySet().stream().map(
×
178
                                        e -> format("<%s>; rel=\"%s\"", e.getValue(), e.getKey()))
×
179
                                        .collect(toList())));
×
180
                }
181
                return r.build();
×
182
        }
183

184
        @Override
185
        public void listJobs(boolean wait, boolean destroyed, int limit, int start,
186
                        UriInfo ui, AsyncResponse response) {
187
                if (limit > LIMIT_LIMIT || limit < 1) {
×
188
                        throw new BadArgs("limit must not be bigger than " + LIMIT_LIMIT);
×
189
                }
190
                if (start < 0) {
×
191
                        throw new BadArgs("start must not be less than 0");
×
192
                }
193
                var jc = core.getJobs(destroyed, limit, start);
×
194
                if (wait) {
×
195
                        bgAction(response, () -> {
×
196
                                log.debug("starting wait for change of job list");
×
197
                                jc.waitForChange(properties.getWait());
×
198
                                var newJc = core.getJobs(destroyed, limit, start);
×
199
                                return wrapPaging(new ListJobsResponse(newJc, ui), ui, start,
×
200
                                                limit);
201
                        });
202
                } else {
203
                        fgAction(response, () -> wrapPaging(new ListJobsResponse(jc, ui),
×
204
                                        ui, start, limit));
205
                }
206
        }
×
207

208
        private static String trim(String str) {
209
                if (isNull(str)) {
×
210
                        return null;
×
211
                }
212
                return str.strip();
×
213
        }
214

215
        /**
216
         * Select one of the ways to create a job based on the request parameters.
217
         * <p>
218
         * <strong>Note</strong> that this will pick the first non-{@code null} of
219
         * (group, nmpiCollabId, nmpiJobId) from req to determine the spalloc call
220
         * to make (also acceptable if all are {@code null}). Validation
221
         * <em>should</em> ensure that at most one of those is non-{@code null}.
222
         *
223
         * @param req
224
         *            The request details.
225
         * @param crds
226
         *            The request credentials.
227
         * @return The job created.
228
         * @throws JsonProcessingException
229
         *             If there is an error converting the request into bytes.
230
         */
231
        private Optional<SpallocAPI.Job> createJob(CreateJobRequest req,
232
                        CreateDescriptor crds) throws JsonProcessingException {
233
                if (!isNull(req.group())) {
×
234
                        return core.createJobInGroup(trim(req.owner()), trim(req.group()),
×
235
                                        crds, req.machineName(), req.tags(),
×
236
                                        req.keepaliveInterval(), mapper.writeValueAsBytes(req));
×
237
                } else if (!isNull(req.nmpiCollab())) {
×
238
                        return core.createJobInCollabSession(trim(req.owner()),
×
239
                                        trim(req.nmpiCollab()), crds, req.machineName(), req.tags(),
×
240
                                        req.keepaliveInterval(), mapper.writeValueAsBytes(req));
×
241
                } else if (!isNull(req.nmpiJobId())) {
×
242
                        return core.createJobForNMPIJob(trim(req.owner()), req.nmpiJobId(),
×
243
                                        crds, req.machineName(), req.tags(),
×
244
                                        req.keepaliveInterval(), mapper.writeValueAsBytes(req));
×
245
                } else {
246
                        return core.createJob(trim(req.owner()), crds, req.machineName(),
×
247
                                        req.tags(), req.keepaliveInterval(),
×
248
                                        mapper.writeValueAsBytes(req));
×
249
                }
250
        }
251

252
        @Override
253
        public void createJob(CreateJobRequest req, UriInfo ui,
254
                        SecurityContext security, AsyncResponse response) {
255
                var r = validateCreateJobNonSizeAttrs(req, security);
×
256
                var crds = validateAndApplyDefaultsToJobRequest(r, security);
×
257

258
                bgAction(response, () -> ifElse(
×
259
                                createJob(r, crds),
×
260
                                job -> created(ui.getRequestUriBuilder().path("{id}")
×
261
                                                .build(job.getId()))
×
262
                                                .entity(new CreateJobResponse(job, ui)).build(),
×
263
                                () -> status(BAD_REQUEST).type(TEXT_PLAIN)
×
264
                                                // Most likely reason for failure
265
                                                .entity("out of quota").build()));
×
266
        }
×
267

268
        private CreateJobRequest validateCreateJobNonSizeAttrs(CreateJobRequest req,
269
                        SecurityContext security) {
270
                if (isNull(req)) {
×
271
                        throw new BadArgs("request must be supplied");
×
272
                }
273

274
                var owner = req.owner();
×
275
                if (!security.isUserInRole("ADMIN")
×
276
                                && !security.isUserInRole("NMPI_EXEC")
×
277
                                && !isNull(owner) && !owner.isBlank()) {
×
278
                        throw new BadArgs("Only admin and NMPI users can specify an owner");
×
279
                }
280

281
                if (isNull(owner) || owner.isBlank()) {
×
282
                        owner = security.getUserPrincipal().getName();
×
283
                }
284
                if (isNull(owner) || owner.isBlank()) {
×
285
                        throw new BadArgs(
×
286
                                        "request must be connected to an identified owner");
287
                }
288
                owner = owner.strip();
×
289

290
                var ka = properties.getKeepalive();
×
291
                if (isNull(req.keepaliveInterval())
×
292
                                || req.keepaliveInterval().compareTo(ka.getMin()) < 0) {
×
293
                        throw new BadArgs(
×
294
                                        "keepalive interval must be at least " + ka.getMin());
×
295
                }
296
                if (req.keepaliveInterval().compareTo(ka.getMax()) > 0) {
×
297
                        throw new BadArgs(
×
298
                                        "keepalive interval must be no more than " + ka.getMax());
×
299
                }
300

301
                var tags = req.tags();
×
302
                if (isNull(tags)) {
×
303
                        tags = new ArrayList<>();
×
304
                        if (isNull(req.machineName())) {
×
305
                                tags.add("default");
×
306
                        }
307
                }
308
                if (nonNull(req.machineName()) && !tags.isEmpty()) {
×
309
                        throw new BadArgs(
×
310
                                        "must not specify machine name and tags together");
311
                }
312

313
                return new CreateJobRequest(owner, req.group(), req.nmpiCollab(),
×
314
                                req.nmpiJobId(), req.keepaliveInterval(), req.numBoards(),
×
315
                                req.dimensions(), req.board(), req.machineName(), tags,
×
316
                                req.maxDeadBoards());
×
317
        }
318

319
        private CreateDescriptor validateAndApplyDefaultsToJobRequest(
320
                        CreateJobRequest req, SecurityContext security) throws BadArgs {
321
                var maxDead = req.maxDeadBoards();
×
322
                if (isNull(maxDead)) {
×
323
                        maxDead = 0;
×
324
                } else if (maxDead < 0) {
×
325
                        throw new BadArgs(
×
326
                                        "the maximum number of dead boards must not be negative");
327
                }
328

329
                if (nonNull(req.numBoards())) {
×
330
                        return new CreateNumBoards(req.numBoards(), maxDead);
×
331
                } else if (nonNull(req.dimensions())) {
×
332
                        var size = req.dimensions();
×
333
                        var specific = req.board();
×
334
                        if (nonNull(specific)) {
×
335
                                // Both dimensions AND board; rooted rectangle
336
                                if (nonNull(specific.x())) {
×
337
                                        return new CreateDimensionsAt(size.width(), size.height(),
×
338
                                                        specific.x(), specific.y(), specific.z(), maxDead);
×
339
                                } else if (nonNull(specific.cabinet())) {
×
340
                                        return CreateDimensionsAt.physical(size.width(),
×
341
                                                        size.height(), specific.cabinet(), specific.frame(),
×
342
                                                        specific.board(), maxDead);
×
343
                                } else {
344
                                        return new CreateDimensionsAt(size.width(), size.height(),
×
345
                                                        specific.address(), maxDead);
×
346
                                }
347
                        }
348
                        return new CreateDimensions(size.width(), size.height(), maxDead);
×
349
                } else if (nonNull(req.board())) {
×
350
                        var specific = req.board();
×
351
                        if (nonNull(specific.x())) {
×
352
                                return triad(specific.x(), specific.y(), specific.z());
×
353
                        } else if (nonNull(specific.cabinet())) {
×
354
                                return physical(specific.cabinet(), specific.frame(),
×
355
                                                specific.board());
×
356
                        } else {
357
                                return address(specific.address());
×
358
                        }
359
                } else {
360
                        // It's a single board
361
                        return new CreateNumBoards(1, 0);
×
362
                }
363
        }
364
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc