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

mendersoftware / inventory / 1875234494

13 Sep 2024 11:01AM UTC coverage: 74.471% (-16.7%) from 91.217%
1875234494

push

gitlab-ci

web-flow
Merge pull request #463 from mzedel/chore/deprecate

Chore/deprecate

3095 of 4156 relevant lines covered (74.47%)

121.38 hits per line

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

94.35
/api/http/api_inventory.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package http
16

17
import (
18
        "context"
19
        "fmt"
20
        "net/http"
21
        "strconv"
22
        "strings"
23
        "time"
24

25
        "github.com/ant0ine/go-json-rest/rest"
26
        validation "github.com/go-ozzo/ozzo-validation/v4"
27
        "github.com/pkg/errors"
28

29
        "github.com/mendersoftware/go-lib-micro/accesslog"
30
        "github.com/mendersoftware/go-lib-micro/identity"
31
        midentity "github.com/mendersoftware/go-lib-micro/identity"
32
        "github.com/mendersoftware/go-lib-micro/log"
33
        "github.com/mendersoftware/go-lib-micro/requestid"
34
        "github.com/mendersoftware/go-lib-micro/requestlog"
35
        "github.com/mendersoftware/go-lib-micro/rest_utils"
36
        u "github.com/mendersoftware/go-lib-micro/rest_utils"
37

38
        inventory "github.com/mendersoftware/inventory/inv"
39
        "github.com/mendersoftware/inventory/model"
40
        "github.com/mendersoftware/inventory/store"
41
        "github.com/mendersoftware/inventory/utils"
42
)
43

44
const (
45
        uriDevices       = "/api/0.1.0/devices"
46
        uriDevice        = "/api/0.1.0/devices/#id"
47
        uriDeviceTags    = "/api/0.1.0/devices/#id/tags"
48
        uriDeviceGroups  = "/api/0.1.0/devices/#id/group"
49
        uriDeviceGroup   = "/api/0.1.0/devices/#id/group/#name"
50
        uriAttributes    = "/api/0.1.0/attributes"
51
        uriGroups        = "/api/0.1.0/groups"
52
        uriGroupsName    = "/api/0.1.0/groups/#name"
53
        uriGroupsDevices = "/api/0.1.0/groups/#name/devices"
54

55
        apiUrlInternalV1         = "/api/internal/v1/inventory"
56
        uriInternalAlive         = apiUrlInternalV1 + "/alive"
57
        uriInternalHealth        = apiUrlInternalV1 + "/health"
58
        uriInternalTenants       = apiUrlInternalV1 + "/tenants"
59
        uriInternalDevices       = apiUrlInternalV1 + "/tenants/#tenant_id/devices"
60
        urlInternalDevicesStatus = apiUrlInternalV1 + "/tenants/#tenant_id/devices/status/#status"
61
        uriInternalDeviceDetails = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id"
62
        uriInternalDeviceGroups  = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id/groups"
63
        urlInternalAttributes    = apiUrlInternalV1 +
64
                "/tenants/#tenant_id/device/#device_id/attribute/scope/#scope"
65
        urlInternalReindex   = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id/reindex"
66
        apiUrlManagementV2   = "/api/management/v2/inventory"
67
        urlFiltersAttributes = apiUrlManagementV2 + "/filters/attributes"
68
        urlFiltersSearch     = apiUrlManagementV2 + "/filters/search"
69

70
        apiUrlInternalV2         = "/api/internal/v2/inventory"
71
        urlInternalFiltersSearch = apiUrlInternalV2 + "/tenants/#tenant_id/filters/search"
72

73
        hdrTotalCount = "X-Total-Count"
74
)
75

76
const (
77
        queryParamGroup          = "group"
78
        queryParamSort           = "sort"
79
        queryParamHasGroup       = "has_group"
80
        queryParamValueSeparator = ":"
81
        queryParamScopeSeparator = "/"
82
        sortOrderAsc             = "asc"
83
        sortOrderDesc            = "desc"
84
        sortAttributeNameIdx     = 0
85
        sortOrderIdx             = 1
86
)
87

88
const (
89
        DefaultTimeout = time.Second * 10
90
)
91

92
const (
93
        checkInTimeParamName  = "check_in_time"
94
        checkInTimeParamScope = "system"
95
)
96

97
// model of device's group name response at /devices/:id/group endpoint
98
type InventoryApiGroup struct {
99
        Group model.GroupName `json:"group"`
100
}
101

102
func (g InventoryApiGroup) Validate() error {
56✔
103
        return g.Group.Validate()
56✔
104
}
56✔
105

106
type inventoryHandlers struct {
107
        inventory inventory.InventoryApp
108
}
109

110
// return an ApiHandler for device admission app
111
func NewInventoryApiHandlers(i inventory.InventoryApp) ApiHandler {
139✔
112
        return &inventoryHandlers{
139✔
113
                inventory: i,
139✔
114
        }
139✔
115
}
139✔
116

117
func wrapRoutes(middleware rest.Middleware, routes ...*rest.Route) []*rest.Route {
139✔
118
        for _, route := range routes {
4,031✔
119
                route.Func = middleware.MiddlewareFunc(route.Func)
3,892✔
120
        }
3,892✔
121
        return routes
139✔
122
}
123

124
func (i *inventoryHandlers) Build() (http.Handler, error) {
139✔
125
        //this will override the framework's error resp to the desired one:
139✔
126
        // {"error": "msg"}
139✔
127
        // instead of:
139✔
128
        // {"Error": "msg"}
139✔
129
        rest.ErrorFieldName = "error"
139✔
130

139✔
131
        api := rest.NewApi()
139✔
132

139✔
133
        api.Use(
139✔
134
                &requestlog.RequestLogMiddleware{},
139✔
135
                &requestid.RequestIdMiddleware{},
139✔
136
                &accesslog.AccessLogMiddleware{
139✔
137
                        Format: accesslog.SimpleLogFormat,
139✔
138
                        DisableLog: func(statusCode int, r *rest.Request) bool {
474✔
139
                                if r.URL.Path == uriInternalAlive ||
335✔
140
                                        r.URL.Path == uriInternalHealth &&
335✔
141
                                                statusCode < 300 {
337✔
142
                                        // Skips the health/liveliness probes
2✔
143
                                        return true
2✔
144
                                }
2✔
145
                                return false
333✔
146
                        },
147
                },
148
                &rest.ContentTypeCheckerMiddleware{},
149
        )
150
        internalRoutes := []*rest.Route{
139✔
151
                rest.Get(uriInternalAlive, i.LivelinessHandler),
139✔
152
                rest.Get(uriInternalHealth, i.HealthCheckHandler),
139✔
153

139✔
154
                rest.Patch(urlInternalAttributes, i.PatchDeviceAttributesInternalHandler),
139✔
155
                rest.Post(urlInternalReindex, i.ReindexDeviceDataHandler),
139✔
156

139✔
157
                rest.Post(uriInternalTenants, i.CreateTenantHandler),
139✔
158
                rest.Post(uriInternalDevices, i.AddDeviceHandler),
139✔
159
                rest.Delete(uriInternalDeviceDetails, i.DeleteDeviceHandler),
139✔
160
                rest.Post(urlInternalDevicesStatus, i.InternalDevicesStatusHandler),
139✔
161
                rest.Get(uriInternalDeviceGroups, i.GetDeviceGroupsInternalHandler),
139✔
162
                rest.Post(urlInternalFiltersSearch, i.InternalFiltersSearchHandler),
139✔
163
        }
139✔
164

139✔
165
        publicRoutes := AutogenOptionsRoutes([]*rest.Route{
139✔
166
                rest.Get(uriDevices, i.GetDevicesHandler),
139✔
167
                rest.Get(uriDevice, i.GetDeviceHandler),
139✔
168
                rest.Delete(uriDevice, i.DeleteDeviceInventoryHandler),
139✔
169
                rest.Delete(uriDeviceGroup, i.DeleteDeviceGroupHandler),
139✔
170
                rest.Delete(uriGroupsName, i.DeleteGroupHandler),
139✔
171
                rest.Delete(uriGroupsDevices, i.ClearDevicesGroupHandler),
139✔
172
                rest.Patch(uriAttributes, i.UpdateDeviceAttributesHandler),
139✔
173
                rest.Put(uriAttributes, i.UpdateDeviceAttributesHandler),
139✔
174
                rest.Put(uriDeviceGroups, i.AddDeviceToGroupHandler),
139✔
175
                rest.Patch(uriGroupsDevices, i.AppendDevicesToGroup),
139✔
176
                rest.Put(uriDeviceTags, i.UpdateDeviceTagsHandler),
139✔
177
                rest.Patch(uriDeviceTags, i.UpdateDeviceTagsHandler),
139✔
178

139✔
179
                rest.Get(uriDeviceGroups, i.GetDeviceGroupHandler),
139✔
180
                rest.Get(uriGroups, i.GetGroupsHandler),
139✔
181
                rest.Get(uriGroupsDevices, i.GetDevicesByGroupHandler),
139✔
182

139✔
183
                rest.Get(urlFiltersAttributes, i.FiltersAttributesHandler),
139✔
184
                rest.Post(urlFiltersSearch, i.FiltersSearchHandler),
139✔
185
        }, AllowHeaderOptionsGenerator)
139✔
186
        publicRoutes = wrapRoutes(&identity.IdentityMiddleware{
139✔
187
                UpdateLogger: true,
139✔
188
        }, publicRoutes...)
139✔
189
        routes := append(internalRoutes, publicRoutes...)
139✔
190

139✔
191
        app, err := rest.MakeRouter(routes...)
139✔
192
        if err != nil {
139✔
193
                return nil, errors.Wrap(err, "failed to create router")
×
194
        }
×
195
        api.SetApp(app)
139✔
196

139✔
197
        return api.MakeHandler(), nil
139✔
198

199
}
200

201
func (i *inventoryHandlers) LivelinessHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
202
        w.WriteHeader(http.StatusNoContent)
1✔
203
}
1✔
204

205
func (i *inventoryHandlers) HealthCheckHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
206
        ctx := r.Context()
2✔
207
        l := log.FromContext(ctx)
2✔
208

2✔
209
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
2✔
210
        defer cancel()
2✔
211

2✔
212
        err := i.inventory.HealthCheck(ctx)
2✔
213
        if err != nil {
3✔
214
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
215
                return
1✔
216
        }
1✔
217

218
        w.WriteHeader(http.StatusNoContent)
1✔
219
}
220

221
// `sort` paramater value is an attribute name with optional direction (desc or asc)
222
// separated by colon (:)
223
//
224
// eg. `sort=attr_name1` or `sort=attr_name1:asc`
225
func parseSortParam(r *rest.Request) (*store.Sort, error) {
17✔
226
        sortStr, err := utils.ParseQueryParmStr(r, queryParamSort, false, nil)
17✔
227
        if err != nil {
17✔
228
                return nil, err
×
229
        }
×
230
        if sortStr == "" {
28✔
231
                return nil, nil
11✔
232
        }
11✔
233
        sortValArray := strings.Split(sortStr, queryParamValueSeparator)
6✔
234
        attrNameWithScope := strings.SplitN(
6✔
235
                sortValArray[sortAttributeNameIdx],
6✔
236
                queryParamScopeSeparator,
6✔
237
                2,
6✔
238
        )
6✔
239
        var scope, attrName string
6✔
240
        if len(attrNameWithScope) == 1 {
12✔
241
                scope = model.AttrScopeInventory
6✔
242
                attrName = attrNameWithScope[0]
6✔
243
        } else {
6✔
244
                scope = attrNameWithScope[0]
×
245
                attrName = attrNameWithScope[1]
×
246
        }
×
247
        sort := store.Sort{AttrName: attrName, AttrScope: scope}
6✔
248
        if len(sortValArray) == 2 {
12✔
249
                sortOrder := sortValArray[sortOrderIdx]
6✔
250
                if sortOrder != sortOrderAsc && sortOrder != sortOrderDesc {
7✔
251
                        return nil, errors.New("invalid sort order")
1✔
252
                }
1✔
253
                sort.Ascending = sortOrder == sortOrderAsc
5✔
254
        }
255
        return &sort, nil
5✔
256
}
257

258
// Filter paramaters name are attributes name. Value can be prefixed
259
// with equality operator code (`eq` for =), separated from value by colon (:).
260
// Equality operator default value is `eq`
261
//
262
// eg. `attr_name1=value1` or `attr_name1=eq:value1`
263
func parseFilterParams(r *rest.Request) ([]store.Filter, error) {
25✔
264
        knownParams := []string{
25✔
265
                utils.PageName,
25✔
266
                utils.PerPageName,
25✔
267
                queryParamSort,
25✔
268
                queryParamHasGroup,
25✔
269
                queryParamGroup,
25✔
270
        }
25✔
271
        filters := make([]store.Filter, 0)
25✔
272
        var filter store.Filter
25✔
273
        for name := range r.URL.Query() {
86✔
274
                if utils.ContainsString(name, knownParams) {
110✔
275
                        continue
49✔
276
                }
277
                valueStr, err := utils.ParseQueryParmStr(r, name, false, nil)
12✔
278
                if err != nil {
12✔
279
                        return nil, err
×
280
                }
×
281

282
                attrNameWithScope := strings.SplitN(name, queryParamScopeSeparator, 2)
12✔
283
                var scope, attrName string
12✔
284
                if len(attrNameWithScope) == 1 {
22✔
285
                        scope = model.AttrScopeInventory
10✔
286
                        attrName = attrNameWithScope[0]
10✔
287
                } else {
12✔
288
                        scope = attrNameWithScope[0]
2✔
289
                        attrName = attrNameWithScope[1]
2✔
290
                }
2✔
291
                filter = store.Filter{AttrName: attrName, AttrScope: scope}
12✔
292

12✔
293
                // make sure we parse ':'s in value, it's either:
12✔
294
                // not there
12✔
295
                // after a valid operator specifier
12✔
296
                // or/and inside the value itself(mac, etc), in which case leave it alone
12✔
297
                sepIdx := strings.Index(valueStr, ":")
12✔
298
                if sepIdx == -1 {
16✔
299
                        filter.Value = valueStr
4✔
300
                        filter.Operator = store.Eq
4✔
301
                } else {
12✔
302
                        validOps := []string{"eq"}
8✔
303
                        for _, o := range validOps {
16✔
304
                                if valueStr[:sepIdx] == o {
12✔
305
                                        switch o {
4✔
306
                                        case "eq":
4✔
307
                                                filter.Operator = store.Eq
4✔
308
                                                filter.Value = valueStr[sepIdx+1:]
4✔
309
                                        }
310
                                        break
4✔
311
                                }
312
                        }
313

314
                        if filter.Value == "" {
12✔
315
                                filter.Value = valueStr
4✔
316
                                filter.Operator = store.Eq
4✔
317
                        }
4✔
318
                }
319

320
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
12✔
321
                if err == nil {
15✔
322
                        filter.ValueFloat = &floatValue
3✔
323
                }
3✔
324

325
                timeValue, err := time.Parse("2006-01-02T15:04:05Z", filter.Value)
12✔
326
                if err == nil {
14✔
327
                        filter.ValueTime = &timeValue
2✔
328
                }
2✔
329

330
                filters = append(filters, filter)
12✔
331
        }
332
        return filters, nil
25✔
333
}
334

335
func (i *inventoryHandlers) GetDevicesHandler(w rest.ResponseWriter, r *rest.Request) {
21✔
336
        ctx := r.Context()
21✔
337

21✔
338
        l := log.FromContext(ctx)
21✔
339

21✔
340
        page, perPage, err := utils.ParsePagination(r)
21✔
341
        if err != nil {
24✔
342
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
343
                return
3✔
344
        }
3✔
345

346
        hasGroup, err := utils.ParseQueryParmBool(r, queryParamHasGroup, false, nil)
18✔
347
        if err != nil {
19✔
348
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
349
                return
1✔
350
        }
1✔
351

352
        groupName, err := utils.ParseQueryParmStr(r, "group", false, nil)
17✔
353
        if err != nil {
17✔
354
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
355
                return
×
356
        }
×
357

358
        sort, err := parseSortParam(r)
17✔
359
        if err != nil {
18✔
360
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
361
                return
1✔
362
        }
1✔
363

364
        filters, err := parseFilterParams(r)
16✔
365
        if err != nil {
16✔
366
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
367
                return
×
368
        }
×
369

370
        ld := store.ListQuery{Skip: int((page - 1) * perPage),
16✔
371
                Limit:     int(perPage),
16✔
372
                Filters:   filters,
16✔
373
                Sort:      sort,
16✔
374
                HasGroup:  hasGroup,
16✔
375
                GroupName: groupName}
16✔
376

16✔
377
        devs, totalCount, err := i.inventory.ListDevices(ctx, ld)
16✔
378

16✔
379
        if err != nil {
17✔
380
                u.RestErrWithLogInternal(w, r, l, err)
1✔
381
                return
1✔
382
        }
1✔
383

384
        hasNext := totalCount > int(page*perPage)
15✔
385
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
15✔
386
        for _, l := range links {
38✔
387
                w.Header().Add("Link", l)
23✔
388
        }
23✔
389
        // the response writer will ensure the header name is in Kebab-Pascal-Case
390
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
15✔
391
        _ = w.WriteJson(devs)
15✔
392
}
393

394
func (i *inventoryHandlers) GetDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
15✔
395
        ctx := r.Context()
15✔
396

15✔
397
        l := log.FromContext(ctx)
15✔
398

15✔
399
        deviceID := r.PathParam("id")
15✔
400

15✔
401
        dev, err := i.inventory.GetDevice(ctx, model.DeviceID(deviceID))
15✔
402
        if err != nil {
16✔
403
                u.RestErrWithLogInternal(w, r, l, err)
1✔
404
                return
1✔
405
        }
1✔
406
        if dev == nil {
15✔
407
                u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
408
                return
1✔
409
        }
1✔
410
        if dev.TagsEtag != "" {
22✔
411
                w.Header().Set("ETag", dev.TagsEtag)
9✔
412
        }
9✔
413

414
        _ = w.WriteJson(dev)
13✔
415
}
416

417
func (i *inventoryHandlers) DeleteDeviceInventoryHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
418
        ctx := r.Context()
3✔
419

3✔
420
        l := log.FromContext(ctx)
3✔
421

3✔
422
        deviceID := r.PathParam("id")
3✔
423

3✔
424
        err := i.inventory.ReplaceAttributes(ctx, model.DeviceID(deviceID),
3✔
425
                model.DeviceAttributes{}, model.AttrScopeInventory, "")
3✔
426
        if err != nil && err != store.ErrDevNotFound {
4✔
427
                u.RestErrWithLogInternal(w, r, l, err)
1✔
428
                return
1✔
429
        }
1✔
430

431
        w.WriteHeader(http.StatusNoContent)
2✔
432
}
433

434
func (i *inventoryHandlers) DeleteDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
435
        ctx := r.Context()
3✔
436
        tenantId := r.PathParam("tenant_id")
3✔
437
        if tenantId != "" {
6✔
438
                id := &midentity.Identity{
3✔
439
                        Tenant: tenantId,
3✔
440
                }
3✔
441
                ctx = midentity.WithContext(ctx, id)
3✔
442
        }
3✔
443

444
        l := log.FromContext(ctx)
3✔
445

3✔
446
        deviceID := r.PathParam("device_id")
3✔
447

3✔
448
        err := i.inventory.DeleteDevice(ctx, model.DeviceID(deviceID))
3✔
449
        if err != nil && err != store.ErrDevNotFound {
4✔
450
                u.RestErrWithLogInternal(w, r, l, err)
1✔
451
                return
1✔
452
        }
1✔
453

454
        w.WriteHeader(http.StatusNoContent)
2✔
455
}
456

457
func (i *inventoryHandlers) AddDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
85✔
458
        ctx := r.Context()
85✔
459
        tenantId := r.PathParam("tenant_id")
85✔
460
        if tenantId != "" {
94✔
461
                id := &midentity.Identity{
9✔
462
                        Tenant: tenantId,
9✔
463
                }
9✔
464
                ctx = midentity.WithContext(ctx, id)
9✔
465
        }
9✔
466

467
        l := log.FromContext(ctx)
85✔
468

85✔
469
        dev, err := parseDevice(r)
85✔
470
        if err != nil {
91✔
471
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
6✔
472
                return
6✔
473
        }
6✔
474

475
        err = dev.Attributes.Validate()
79✔
476
        if err != nil {
79✔
477
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
478
                return
×
479
        }
×
480

481
        err = i.inventory.AddDevice(ctx, dev)
79✔
482
        if err != nil {
80✔
483
                u.RestErrWithLogInternal(w, r, l, err)
1✔
484
                return
1✔
485
        }
1✔
486

487
        w.Header().Add("Location", "devices/"+dev.ID.String())
78✔
488
        w.WriteHeader(http.StatusCreated)
78✔
489
}
490

491
func (i *inventoryHandlers) UpdateDeviceAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
13✔
492
        ctx := r.Context()
13✔
493
        l := log.FromContext(ctx)
13✔
494
        var idata *identity.Identity
13✔
495
        if idata = identity.FromContext(ctx); idata == nil || !idata.IsDevice {
15✔
496
                u.RestErrWithLog(w, r, l, errors.New("unauthorized"), http.StatusUnauthorized)
2✔
497
                return
2✔
498
        }
2✔
499
        deviceID := model.DeviceID(idata.Subject)
11✔
500
        //extract attributes from body
11✔
501
        attrs, err := parseAttributes(r)
11✔
502
        if err != nil {
15✔
503
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
504
                return
4✔
505
        }
4✔
506
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeInventory, "")
7✔
507
}
508

509
func (i *inventoryHandlers) UpdateDeviceTagsHandler(w rest.ResponseWriter, r *rest.Request) {
18✔
510
        ctx := r.Context()
18✔
511
        l := log.FromContext(ctx)
18✔
512

18✔
513
        // get device ID from uri
18✔
514
        deviceID := model.DeviceID(r.PathParam("id"))
18✔
515
        if len(deviceID) < 1 {
18✔
516
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
×
517
                return
×
518
        }
×
519

520
        ifMatchHeader := r.Header.Get("If-Match")
18✔
521

18✔
522
        // extract attributes from body
18✔
523
        attrs, err := parseAttributes(r)
18✔
524
        if err != nil {
18✔
525
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
526
                return
×
527
        }
×
528

529
        // set scope and timestamp for tags attributes
530
        now := time.Now()
18✔
531
        for i := range attrs {
63✔
532
                attrs[i].Scope = model.AttrScopeTags
45✔
533
                if attrs[i].Timestamp == nil {
90✔
534
                        attrs[i].Timestamp = &now
45✔
535
                }
45✔
536
        }
537

538
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeTags, ifMatchHeader)
18✔
539
}
540

541
func (i *inventoryHandlers) updateDeviceAttributes(
542
        w rest.ResponseWriter,
543
        r *rest.Request,
544
        ctx context.Context,
545
        attrs model.DeviceAttributes,
546
        deviceID model.DeviceID,
547
        scope string,
548
        etag string,
549
) {
25✔
550
        l := log.FromContext(ctx)
25✔
551
        var err error
25✔
552

25✔
553
        // upsert or replace the attributes
25✔
554
        if r.Method == http.MethodPatch {
40✔
555
                err = i.inventory.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
15✔
556
        } else if r.Method == http.MethodPut {
35✔
557
                err = i.inventory.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
10✔
558
        } else {
10✔
559
                u.RestErrWithLog(w, r, l, errors.New("method not alllowed"), http.StatusMethodNotAllowed)
×
560
                return
×
561
        }
×
562

563
        cause := errors.Cause(err)
25✔
564
        switch cause {
25✔
565
        case store.ErrNoAttrName:
×
566
        case inventory.ErrTooManyAttributes:
1✔
567
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
1✔
568
                return
1✔
569
        case inventory.ErrETagDoesntMatch:
4✔
570
                u.RestErrWithInfoMsg(w, r, l, cause, http.StatusPreconditionFailed, cause.Error())
4✔
571
                return
4✔
572
        }
573
        if err != nil {
21✔
574
                u.RestErrWithLogInternal(w, r, l, err)
1✔
575
                return
1✔
576
        }
1✔
577

578
        w.WriteHeader(http.StatusOK)
19✔
579
}
580

581
func (i *inventoryHandlers) PatchDeviceAttributesInternalHandler(
582
        w rest.ResponseWriter,
583
        r *rest.Request,
584
) {
11✔
585
        ctx := r.Context()
11✔
586
        tenantId := r.PathParam("tenant_id")
11✔
587
        ctx = getTenantContext(ctx, tenantId)
11✔
588

11✔
589
        l := log.FromContext(ctx)
11✔
590

11✔
591
        deviceId := r.PathParam("device_id")
11✔
592
        if len(deviceId) < 1 {
12✔
593
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
594
                return
1✔
595
        }
1✔
596
        //extract attributes from body
597
        attrs, err := parseAttributes(r)
10✔
598
        if err != nil {
14✔
599
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
600
                return
4✔
601
        }
4✔
602
        for i := range attrs {
18✔
603
                attrs[i].Scope = r.PathParam("scope")
12✔
604
                if attrs[i].Name == checkInTimeParamName && attrs[i].Scope == checkInTimeParamScope {
12✔
605
                        t, err := time.Parse(time.RFC3339, fmt.Sprintf("%v", attrs[i].Value))
×
606
                        if err != nil {
×
607
                                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
608
                                return
×
609
                        }
×
610
                        attrs[i].Value = t
×
611
                }
612
        }
613

614
        //upsert the attributes
615
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
6✔
616
        cause := errors.Cause(err)
6✔
617
        switch cause {
6✔
618
        case store.ErrNoAttrName:
×
619
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
620
                return
×
621
        }
622
        if err != nil {
7✔
623
                u.RestErrWithLogInternal(w, r, l, err)
1✔
624
                return
1✔
625
        }
1✔
626

627
        w.WriteHeader(http.StatusOK)
5✔
628
}
629

630
func (i *inventoryHandlers) DeleteDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
631
        ctx := r.Context()
7✔
632

7✔
633
        l := log.FromContext(ctx)
7✔
634

7✔
635
        deviceID := r.PathParam("id")
7✔
636
        groupName := r.PathParam("name")
7✔
637

7✔
638
        err := i.inventory.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
7✔
639
        if err != nil {
11✔
640
                cause := errors.Cause(err)
4✔
641
                if cause != nil {
8✔
642
                        if cause.Error() == store.ErrDevNotFound.Error() {
7✔
643
                                u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
3✔
644
                                return
3✔
645
                        }
3✔
646
                }
647
                u.RestErrWithLogInternal(w, r, l, err)
1✔
648
                return
1✔
649
        }
650

651
        w.WriteHeader(http.StatusNoContent)
3✔
652
}
653

654
func (i *inventoryHandlers) AddDeviceToGroupHandler(w rest.ResponseWriter, r *rest.Request) {
57✔
655
        ctx := r.Context()
57✔
656

57✔
657
        l := log.FromContext(ctx)
57✔
658

57✔
659
        devId := r.PathParam("id")
57✔
660

57✔
661
        var group InventoryApiGroup
57✔
662
        err := r.DecodeJsonPayload(&group)
57✔
663
        if err != nil {
58✔
664
                u.RestErrWithLog(
1✔
665
                        w, r, l, errors.Wrap(err, "failed to decode device group data"),
1✔
666
                        http.StatusBadRequest)
1✔
667
                return
1✔
668
        }
1✔
669

670
        if err = group.Validate(); err != nil {
59✔
671
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
672
                return
3✔
673
        }
3✔
674

675
        err = i.inventory.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
53✔
676
        if err != nil {
55✔
677
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
3✔
678
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
1✔
679
                        return
1✔
680
                }
1✔
681
                u.RestErrWithLogInternal(w, r, l, err)
1✔
682
                return
1✔
683
        }
684
        w.WriteHeader(http.StatusNoContent)
51✔
685
}
686

687
func (i *inventoryHandlers) GetDevicesByGroupHandler(w rest.ResponseWriter, r *rest.Request) {
29✔
688
        ctx := r.Context()
29✔
689

29✔
690
        l := log.FromContext(ctx)
29✔
691

29✔
692
        group := r.PathParam("name")
29✔
693

29✔
694
        page, perPage, err := utils.ParsePagination(r)
29✔
695
        if err != nil {
32✔
696
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
697
                return
3✔
698
        }
3✔
699

700
        //get one extra device to see if there's a 'next' page
701
        ids, totalCount, err := i.inventory.ListDevicesByGroup(
26✔
702
                ctx,
26✔
703
                model.GroupName(group),
26✔
704
                int((page-1)*perPage),
26✔
705
                int(perPage),
26✔
706
        )
26✔
707
        if err != nil {
32✔
708
                if err == store.ErrGroupNotFound {
11✔
709
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
5✔
710
                } else {
6✔
711
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
712
                }
1✔
713
                return
6✔
714
        }
715

716
        hasNext := totalCount > int(page*perPage)
20✔
717

20✔
718
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
20✔
719
        for _, l := range links {
43✔
720
                w.Header().Add("Link", l)
23✔
721
        }
23✔
722
        // the response writer will ensure the header name is in Kebab-Pascal-Case
723
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
20✔
724
        _ = w.WriteJson(ids)
20✔
725
}
726

727
func (i *inventoryHandlers) AppendDevicesToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
728
        var deviceIDs []model.DeviceID
5✔
729
        ctx := r.Context()
5✔
730
        l := log.FromContext(ctx)
5✔
731
        groupName := model.GroupName(r.PathParam("name"))
5✔
732
        if err := groupName.Validate(); err != nil {
6✔
733
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
734
                return
1✔
735
        }
1✔
736

737
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
738
                u.RestErrWithLog(w, r, l,
1✔
739
                        errors.Wrap(err, "invalid payload schema"),
1✔
740
                        http.StatusBadRequest,
1✔
741
                )
1✔
742
                return
1✔
743
        } else if len(deviceIDs) == 0 {
5✔
744
                u.RestErrWithLog(w, r, l,
1✔
745
                        errors.New("no device IDs present in payload"),
1✔
746
                        http.StatusBadRequest,
1✔
747
                )
1✔
748
                return
1✔
749
        }
1✔
750
        updated, err := i.inventory.UpdateDevicesGroup(
2✔
751
                ctx, deviceIDs, groupName,
2✔
752
        )
2✔
753
        if err != nil {
3✔
754
                u.RestErrWithLogInternal(w, r, l, err)
1✔
755
                return
1✔
756
        }
1✔
757
        _ = w.WriteJson(updated)
1✔
758
}
759

760
func (i *inventoryHandlers) DeleteGroupHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
761
        ctx := r.Context()
3✔
762
        l := log.FromContext(ctx)
3✔
763

3✔
764
        groupName := model.GroupName(r.PathParam("name"))
3✔
765
        if err := groupName.Validate(); err != nil {
4✔
766
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
767
                return
1✔
768
        }
1✔
769

770
        updated, err := i.inventory.DeleteGroup(ctx, groupName)
2✔
771
        if err != nil {
3✔
772
                u.RestErrWithLogInternal(w, r, l, err)
1✔
773
                return
1✔
774
        }
1✔
775
        w.WriteHeader(http.StatusOK)
1✔
776
        _ = w.WriteJson(updated)
1✔
777
}
778

779
func (i *inventoryHandlers) ClearDevicesGroupHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
780
        var deviceIDs []model.DeviceID
5✔
781
        ctx := r.Context()
5✔
782
        l := log.FromContext(ctx)
5✔
783

5✔
784
        groupName := model.GroupName(r.PathParam("name"))
5✔
785
        if err := groupName.Validate(); err != nil {
6✔
786
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
787
                return
1✔
788
        }
1✔
789

790
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
791
                u.RestErrWithLog(w, r, l,
1✔
792
                        errors.Wrap(err, "invalid payload schema"),
1✔
793
                        http.StatusBadRequest,
1✔
794
                )
1✔
795
                return
1✔
796
        } else if len(deviceIDs) == 0 {
5✔
797
                u.RestErrWithLog(w, r, l,
1✔
798
                        errors.New("no device IDs present in payload"),
1✔
799
                        http.StatusBadRequest,
1✔
800
                )
1✔
801
                return
1✔
802
        }
1✔
803

804
        updated, err := i.inventory.UnsetDevicesGroup(ctx, deviceIDs, groupName)
2✔
805
        if err != nil {
3✔
806
                u.RestErrWithLogInternal(w, r, l, err)
1✔
807
                return
1✔
808
        }
1✔
809
        w.WriteHeader(http.StatusOK)
1✔
810
        _ = w.WriteJson(updated)
1✔
811
}
812

813
func parseDevice(r *rest.Request) (*model.Device, error) {
85✔
814
        dev := model.Device{}
85✔
815

85✔
816
        //decode body
85✔
817
        err := r.DecodeJsonPayload(&dev)
85✔
818
        if err != nil {
88✔
819
                return nil, errors.Wrap(err, "failed to decode request body")
3✔
820
        }
3✔
821

822
        if err := dev.Validate(); err != nil {
85✔
823
                return nil, err
3✔
824
        }
3✔
825

826
        return &dev, nil
79✔
827
}
828

829
func parseAttributes(r *rest.Request) (model.DeviceAttributes, error) {
39✔
830
        var attrs model.DeviceAttributes
39✔
831

39✔
832
        err := r.DecodeJsonPayload(&attrs)
39✔
833
        if err != nil {
43✔
834
                return nil, errors.Wrap(err, "failed to decode request body")
4✔
835
        }
4✔
836

837
        err = attrs.Validate()
35✔
838
        if err != nil {
39✔
839
                return nil, err
4✔
840
        }
4✔
841

842
        return attrs, nil
31✔
843
}
844

845
func (i *inventoryHandlers) GetGroupsHandler(w rest.ResponseWriter, r *rest.Request) {
9✔
846
        var fltr []model.FilterPredicate
9✔
847
        ctx := r.Context()
9✔
848

9✔
849
        l := log.FromContext(ctx)
9✔
850

9✔
851
        query := r.URL.Query()
9✔
852
        status := query.Get("status")
9✔
853
        if status != "" {
10✔
854
                fltr = []model.FilterPredicate{{
1✔
855
                        Attribute: "status",
1✔
856
                        Scope:     "identity",
1✔
857
                        Type:      "$eq",
1✔
858
                        Value:     status,
1✔
859
                }}
1✔
860
        }
1✔
861

862
        groups, err := i.inventory.ListGroups(ctx, fltr)
9✔
863
        if err != nil {
10✔
864
                u.RestErrWithLogInternal(w, r, l, err)
1✔
865
                return
1✔
866
        }
1✔
867

868
        if groups == nil {
9✔
869
                groups = []model.GroupName{}
1✔
870
        }
1✔
871

872
        _ = w.WriteJson(groups)
8✔
873
}
874

875
func (i *inventoryHandlers) GetDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
876
        ctx := r.Context()
4✔
877

4✔
878
        l := log.FromContext(ctx)
4✔
879

4✔
880
        deviceID := r.PathParam("id")
4✔
881

4✔
882
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
883
        if err != nil {
6✔
884
                if err == store.ErrDevNotFound {
3✔
885
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
886
                } else {
2✔
887
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
888
                }
1✔
889
                return
2✔
890
        }
891

892
        ret := map[string]*model.GroupName{"group": nil}
2✔
893

2✔
894
        if group != "" {
3✔
895
                ret["group"] = &group
1✔
896
        }
1✔
897

898
        _ = w.WriteJson(ret)
2✔
899
}
900

901
type newTenantRequest struct {
902
        TenantID string `json:"tenant_id" valid:"required"`
903
}
904

905
func (t newTenantRequest) Validate() error {
7✔
906
        return validation.ValidateStruct(&t,
7✔
907
                validation.Field(&t.TenantID, validation.Required),
7✔
908
        )
7✔
909
}
7✔
910

911
func (i *inventoryHandlers) CreateTenantHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
912
        ctx := r.Context()
8✔
913

8✔
914
        l := log.FromContext(ctx)
8✔
915

8✔
916
        var newTenant newTenantRequest
8✔
917

8✔
918
        if err := r.DecodeJsonPayload(&newTenant); err != nil {
9✔
919
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
920
                return
1✔
921
        }
1✔
922

923
        if err := newTenant.Validate(); err != nil {
9✔
924
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
925
                return
2✔
926
        }
2✔
927

928
        err := i.inventory.CreateTenant(ctx, model.NewTenant{
5✔
929
                ID: newTenant.TenantID,
5✔
930
        })
5✔
931
        if err != nil {
6✔
932
                u.RestErrWithLogInternal(w, r, l, err)
1✔
933
                return
1✔
934
        }
1✔
935

936
        w.WriteHeader(http.StatusCreated)
4✔
937
}
938

939
func (i *inventoryHandlers) FiltersAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
940
        ctx := r.Context()
5✔
941

5✔
942
        l := log.FromContext(ctx)
5✔
943

5✔
944
        // query the database
5✔
945
        attributes, err := i.inventory.GetFiltersAttributes(ctx)
5✔
946
        if err != nil {
6✔
947
                u.RestErrWithLogInternal(w, r, l, err)
1✔
948
                return
1✔
949
        }
1✔
950

951
        // in case of nil make sure we return empty list
952
        if attributes == nil {
6✔
953
                attributes = []model.FilterAttribute{}
2✔
954
        }
2✔
955

956
        _ = w.WriteJson(attributes)
4✔
957
}
958

959
func (i *inventoryHandlers) FiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
960
        ctx := r.Context()
8✔
961

8✔
962
        l := log.FromContext(ctx)
8✔
963

8✔
964
        //extract attributes from body
8✔
965
        searchParams, err := parseSearchParams(r)
8✔
966
        if err != nil {
10✔
967
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
968
                return
2✔
969
        }
2✔
970

971
        // query the database
972
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
6✔
973
        if err != nil {
8✔
974
                if strings.Contains(err.Error(), "BadValue") {
3✔
975
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
976
                } else {
2✔
977
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
978
                }
1✔
979
                return
2✔
980
        }
981

982
        // the response writer will ensure the header name is in Kebab-Pascal-Case
983
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
4✔
984
        _ = w.WriteJson(devs)
4✔
985
}
986

987
func (i *inventoryHandlers) InternalFiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
988
        ctx := r.Context()
5✔
989

5✔
990
        l := log.FromContext(ctx)
5✔
991

5✔
992
        tenantId := r.PathParam("tenant_id")
5✔
993
        if tenantId != "" {
9✔
994
                ctx = getTenantContext(ctx, tenantId)
4✔
995
        }
4✔
996

997
        //extract attributes from body
998
        searchParams, err := parseSearchParams(r)
5✔
999
        if err != nil {
6✔
1000
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1001
                return
1✔
1002
        }
1✔
1003

1004
        // query the database
1005
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
4✔
1006
        if err != nil {
6✔
1007
                if strings.Contains(err.Error(), "BadValue") {
3✔
1008
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1009
                } else {
2✔
1010
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1011
                }
1✔
1012
                return
2✔
1013
        }
1014

1015
        // the response writer will ensure the header name is in Kebab-Pascal-Case
1016
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
1017
        _ = w.WriteJson(devs)
2✔
1018
}
1019

1020
func getTenantContext(ctx context.Context, tenantId string) context.Context {
33✔
1021
        if ctx == nil {
33✔
1022
                ctx = context.Background()
×
1023
        }
×
1024
        if tenantId == "" {
35✔
1025
                return ctx
2✔
1026
        }
2✔
1027
        id := &midentity.Identity{
31✔
1028
                Tenant: tenantId,
31✔
1029
        }
31✔
1030

31✔
1031
        ctx = midentity.WithContext(ctx, id)
31✔
1032

31✔
1033
        return ctx
31✔
1034
}
1035

1036
func (i *inventoryHandlers) InternalDevicesStatusHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
1037
        const (
8✔
1038
                StatusDecommissioned = "decommissioned"
8✔
1039
                StatusAccepted       = "accepted"
8✔
1040
                StatusRejected       = "rejected"
8✔
1041
                StatusPreauthorized  = "preauthorized"
8✔
1042
                StatusPending        = "pending"
8✔
1043
                StatusNoAuth         = "noauth"
8✔
1044
        )
8✔
1045
        var (
8✔
1046
                devices []model.DeviceUpdate
8✔
1047
                result  *model.UpdateResult
8✔
1048
        )
8✔
1049

8✔
1050
        ctx := r.Context()
8✔
1051
        l := log.FromContext(ctx)
8✔
1052

8✔
1053
        tenantID := r.PathParam("tenant_id")
8✔
1054
        ctx = getTenantContext(ctx, tenantID)
8✔
1055

8✔
1056
        status := r.PathParam("status")
8✔
1057

8✔
1058
        err := r.DecodeJsonPayload(&devices)
8✔
1059
        if err != nil {
10✔
1060
                u.RestErrWithLog(w, r, l, errors.Wrap(err, "cant parse devices"), http.StatusBadRequest)
2✔
1061
                return
2✔
1062
        }
2✔
1063

1064
        switch status {
6✔
1065
        case StatusAccepted, StatusPreauthorized,
1066
                StatusPending, StatusRejected,
1067
                StatusNoAuth:
5✔
1068
                // Update statuses
5✔
1069
                attrs := model.DeviceAttributes{{
5✔
1070
                        Name:  "status",
5✔
1071
                        Scope: model.AttrScopeIdentity,
5✔
1072
                        Value: status,
5✔
1073
                }}
5✔
1074
                result, err = i.inventory.UpsertDevicesStatuses(ctx, devices, attrs)
5✔
1075
        case StatusDecommissioned:
×
1076
                // Delete Inventory
×
1077
                result, err = i.inventory.DeleteDevices(ctx, getIdsFromDevices(devices))
×
1078
        default:
1✔
1079
                // Unrecognized status
1✔
1080
                u.RestErrWithLog(w, r, l,
1✔
1081
                        errors.Errorf("unrecognized status: %s", status),
1✔
1082
                        http.StatusNotFound,
1✔
1083
                )
1✔
1084
                return
1✔
1085
        }
1086
        if err == store.ErrWriteConflict {
6✔
1087
                u.RestErrWithLog(w, r, l, err, http.StatusConflict)
1✔
1088
                return
1✔
1089
        } else if err != nil {
6✔
1090
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1091
                return
1✔
1092
        }
1✔
1093

1094
        w.WriteHeader(http.StatusOK)
3✔
1095
        _ = w.WriteJson(result)
3✔
1096
}
1097

1098
func (i *inventoryHandlers) GetDeviceGroupsInternalHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
1099
        ctx := r.Context()
4✔
1100

4✔
1101
        l := log.FromContext(ctx)
4✔
1102

4✔
1103
        tenantId := r.PathParam("tenant_id")
4✔
1104
        ctx = getTenantContext(ctx, tenantId)
4✔
1105

4✔
1106
        deviceID := r.PathParam("device_id")
4✔
1107
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
1108
        if err != nil {
6✔
1109
                if err == store.ErrDevNotFound {
3✔
1110
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
1111
                } else {
2✔
1112
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1113
                }
1✔
1114
                return
2✔
1115
        }
1116

1117
        res := model.DeviceGroups{}
2✔
1118
        if group != "" {
3✔
1119
                res.Groups = append(res.Groups, string(group))
1✔
1120
        }
1✔
1121

1122
        _ = w.WriteJson(res)
2✔
1123
}
1124

1125
func (i *inventoryHandlers) ReindexDeviceDataHandler(w rest.ResponseWriter, r *rest.Request) {
6✔
1126
        ctx := r.Context()
6✔
1127
        tenantId := r.PathParam("tenant_id")
6✔
1128
        ctx = getTenantContext(ctx, tenantId)
6✔
1129

6✔
1130
        l := log.FromContext(ctx)
6✔
1131

6✔
1132
        deviceId := r.PathParam("device_id")
6✔
1133
        if len(deviceId) < 1 {
7✔
1134
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
1135
                return
1✔
1136
        }
1✔
1137

1138
        serviceName, err := utils.ParseQueryParmStr(r, "service", false, nil)
5✔
1139
        // inventory service accepts only reindex requests from devicemonitor
5✔
1140
        if err != nil || serviceName != "devicemonitor" {
6✔
1141
                u.RestErrWithLog(w, r, l, errors.New("unsupported service"), http.StatusBadRequest)
1✔
1142
                return
1✔
1143
        }
1✔
1144

1145
        // check devicemonitor alerts
1146
        alertsCount, err := i.inventory.CheckAlerts(ctx, deviceId)
4✔
1147
        if err != nil {
5✔
1148
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1149
                return
1✔
1150
        }
1✔
1151

1152
        alertsPresent := false
3✔
1153
        if alertsCount > 0 {
4✔
1154
                alertsPresent = true
1✔
1155
        }
1✔
1156
        attrs := model.DeviceAttributes{
3✔
1157
                model.DeviceAttribute{
3✔
1158
                        Name:  model.AttrNameNumberOfAlerts,
3✔
1159
                        Scope: model.AttrScopeMonitor,
3✔
1160
                        Value: alertsCount,
3✔
1161
                },
3✔
1162
                model.DeviceAttribute{
3✔
1163
                        Name:  model.AttrNameAlerts,
3✔
1164
                        Scope: model.AttrScopeMonitor,
3✔
1165
                        Value: alertsPresent,
3✔
1166
                },
3✔
1167
        }
3✔
1168

3✔
1169
        // upsert monitor attributes
3✔
1170
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
3✔
1171
        cause := errors.Cause(err)
3✔
1172
        switch cause {
3✔
1173
        case store.ErrNoAttrName:
×
1174
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
1175
                return
×
1176
        }
1177
        if err != nil {
4✔
1178
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1179
                return
1✔
1180
        }
1✔
1181

1182
        w.WriteHeader(http.StatusOK)
2✔
1183
}
1184

1185
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
×
1186
        ids := make([]model.DeviceID, len(devices))
×
1187
        for i, dev := range devices {
×
1188
                ids[i] = dev.Id
×
1189
        }
×
1190
        return ids
×
1191
}
1192

1193
func parseSearchParams(r *rest.Request) (*model.SearchParams, error) {
19✔
1194
        var searchParams model.SearchParams
19✔
1195

19✔
1196
        if err := r.DecodeJsonPayload(&searchParams); err != nil {
20✔
1197
                return nil, errors.Wrap(err, "failed to decode request body")
1✔
1198
        }
1✔
1199

1200
        if searchParams.Page < 1 {
19✔
1201
                searchParams.Page = utils.PageDefault
1✔
1202
        }
1✔
1203
        if searchParams.PerPage < 1 {
19✔
1204
                searchParams.PerPage = utils.PerPageDefault
1✔
1205
        }
1✔
1206

1207
        if err := searchParams.Validate(); err != nil {
23✔
1208
                return nil, err
5✔
1209
        }
5✔
1210

1211
        return &searchParams, nil
13✔
1212
}
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