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

mendersoftware / iot-manager / 1401702106

05 Aug 2024 08:32PM UTC coverage: 87.577%. Remained the same
1401702106

push

gitlab-ci

web-flow
Merge pull request #295 from mendersoftware/dependabot/docker/docker-dependencies-03b04ac819

chore: bump golang from 1.22.4-alpine3.19 to 1.22.5-alpine3.19 in the docker-dependencies group

3264 of 3727 relevant lines covered (87.58%)

11.44 hits per line

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

92.74
/app/app_iot_hub.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 app
16

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

25
        "github.com/pkg/errors"
26

27
        "github.com/mendersoftware/go-lib-micro/log"
28

29
        "github.com/mendersoftware/iot-manager/client"
30
        "github.com/mendersoftware/iot-manager/client/iothub"
31
        "github.com/mendersoftware/iot-manager/crypto"
32
        "github.com/mendersoftware/iot-manager/model"
33
)
34

35
const (
36
        iotHubMetadata = "$metadata"
37
        iotHubVersion  = "$version"
38
)
39

40
func removeIoTHubMetadata(values map[string]interface{}) map[string]interface{} {
7✔
41
        for key := range values {
16✔
42
                switch key {
9✔
43
                case iotHubMetadata, iotHubVersion:
2✔
44
                        delete(values, key)
2✔
45
                }
46
        }
47
        return values
7✔
48
}
49

50
func (a *app) provisionIoTHubDevice(
51
        ctx context.Context,
52
        deviceID string,
53
        integration model.Integration,
54
        deviceUpdate ...*iothub.Device,
55
) error {
6✔
56
        cs := integration.Credentials.ConnectionString
6✔
57
        if cs == nil {
6✔
58
                return ErrNoCredentials
×
59
        }
×
60

61
        dev, err := a.iothubClient.UpsertDevice(ctx, cs, deviceID, deviceUpdate...)
6✔
62
        if err != nil {
8✔
63
                if htErr, ok := err.(client.HTTPError); ok {
2✔
64
                        switch htErr.Code() {
×
65
                        case http.StatusUnauthorized:
×
66
                                return ErrNoCredentials
×
67
                        case http.StatusConflict:
×
68
                                return ErrDeviceAlreadyExists
×
69
                        }
70
                }
71
                return errors.Wrap(err, "failed to update iothub devices")
2✔
72
        }
73
        if dev.Auth == nil || dev.Auth.SymmetricKey == nil {
5✔
74
                return ErrNoDeviceConnectionString
1✔
75
        }
1✔
76
        primKey := &model.ConnectionString{
3✔
77
                Key:      crypto.String(dev.Auth.SymmetricKey.Primary),
3✔
78
                DeviceID: dev.DeviceID,
3✔
79
                HostName: cs.HostName,
3✔
80
        }
3✔
81

3✔
82
        err = a.wf.ProvisionExternalDevice(ctx, dev.DeviceID, map[string]string{
3✔
83
                confKeyPrimaryKey: primKey.String(),
3✔
84
        })
3✔
85
        if err != nil {
3✔
86
                return errors.Wrap(err, "failed to submit iothub authn to deviceconfig")
×
87
        }
×
88
        err = a.iothubClient.UpdateDeviceTwin(ctx, cs, dev.DeviceID, &iothub.DeviceTwinUpdate{
3✔
89
                Tags: map[string]interface{}{
3✔
90
                        "mender": true,
3✔
91
                },
3✔
92
        })
3✔
93
        return errors.Wrap(err, "failed to tag provisioned iothub device")
3✔
94
}
95

96
func (a *app) setDeviceStatusIoTHub(ctx context.Context, deviceID string, status model.Status,
97
        integration model.Integration) error {
6✔
98
        cs := integration.Credentials.ConnectionString
6✔
99
        if cs == nil {
7✔
100
                return ErrNoCredentials
1✔
101
        }
1✔
102
        azureStatus := iothub.NewStatusFromMenderStatus(status)
5✔
103
        dev, err := a.iothubClient.GetDevice(ctx, cs, deviceID)
5✔
104
        if err != nil {
6✔
105
                return errors.Wrap(err, "failed to retrieve device from IoT Hub")
1✔
106
        } else if dev.Status == azureStatus {
6✔
107
                // We're done...
1✔
108
                return nil
1✔
109
        }
1✔
110

111
        dev.Status = azureStatus
3✔
112
        _, err = a.iothubClient.UpsertDevice(ctx, cs, deviceID, dev)
3✔
113
        return err
3✔
114
}
115

116
func (a *app) decommissionIoTHubDevice(ctx context.Context, deviceID string,
117
        integration model.Integration) error {
7✔
118
        cs := integration.Credentials.ConnectionString
7✔
119
        if cs == nil {
8✔
120
                return ErrNoCredentials
1✔
121
        }
1✔
122
        err := a.iothubClient.DeleteDevice(ctx, cs, deviceID)
6✔
123
        if err != nil {
7✔
124
                if htErr, ok := err.(client.HTTPError); ok &&
1✔
125
                        htErr.Code() == http.StatusNotFound {
1✔
126
                        return nil
×
127
                }
×
128
                return errors.Wrap(err, "failed to delete IoT Hub device")
1✔
129
        }
130
        return nil
5✔
131
}
132

133
func (a *app) syncIoTHubDevices(
134
        ctx context.Context,
135
        deviceIDs []string,
136
        integration model.Integration,
137
        failEarly bool,
138
) error {
8✔
139
        l := log.FromContext(ctx)
8✔
140
        cs := integration.Credentials.ConnectionString
8✔
141

8✔
142
        // Get device authentication
8✔
143
        devAuths, err := a.devauth.GetDevices(ctx, deviceIDs)
8✔
144
        if err != nil {
9✔
145
                return errors.Wrap(err, "app: failed to lookup device authentication")
1✔
146
        }
1✔
147

148
        statuses := make(map[string]iothub.Status, len(deviceIDs))
7✔
149
        for _, auth := range devAuths {
47✔
150
                statuses[auth.ID] = iothub.NewStatusFromMenderStatus(auth.Status)
40✔
151
        }
40✔
152
        // Find devices that shouldn't exist
153
        var (
7✔
154
                i int
7✔
155
                j int = len(deviceIDs)
7✔
156
        )
7✔
157
        for i < j {
53✔
158
                id := deviceIDs[i]
46✔
159
                if _, ok := statuses[id]; !ok {
52✔
160
                        l.Warnf("Device '%s' does not have an auth set: deleting device", id)
6✔
161
                        err := a.decommissionDevice(ctx, id)
6✔
162
                        if err != nil && err != ErrDeviceNotFound {
8✔
163
                                err = errors.Wrap(err, "app: failed to decommission device")
2✔
164
                                if failEarly {
3✔
165
                                        return err
1✔
166
                                }
1✔
167
                                l.Error(err)
1✔
168
                        }
169
                        // swap(deviceIDs[i], deviceIDs[j])
170
                        j--
5✔
171
                        tmp := deviceIDs[i]
5✔
172
                        deviceIDs[i] = deviceIDs[j]
5✔
173
                        deviceIDs[j] = tmp
5✔
174
                } else {
40✔
175
                        i++
40✔
176
                }
40✔
177
        }
178

179
        // Fetch IoT Hub device twins
180
        hubDevs, err := a.iothubClient.GetDeviceTwins(ctx, cs, deviceIDs[:j])
6✔
181
        if err != nil {
7✔
182
                return errors.Wrap(err, "app: failed to get devices from IoT Hub")
1✔
183
        }
1✔
184

185
        // Set of device IDs in iot hub
186
        devicesInHub := make(map[string]struct{}, len(hubDevs))
5✔
187

5✔
188
        // Check if devices (statuses) are in sync
5✔
189
        for _, twin := range hubDevs {
42✔
190
                devicesInHub[twin.DeviceID] = struct{}{}
37✔
191
                if stat, ok := statuses[twin.DeviceID]; ok {
73✔
192
                        if stat == twin.Status {
65✔
193
                                continue
29✔
194
                        }
195
                        l.Warnf("Device '%s' status does not match Mender auth status, updating status",
7✔
196
                                twin.DeviceID)
7✔
197
                        // Update the device's status
7✔
198
                        // NOTE need to fetch device identity first
7✔
199
                        dev, err := a.iothubClient.GetDevice(ctx, cs, twin.DeviceID)
7✔
200
                        if err != nil {
8✔
201
                                err = errors.Wrap(err, "failed to retrieve IoT Hub device identity")
1✔
202
                                if failEarly {
1✔
203
                                        return err
×
204
                                }
×
205
                                l.Error(err)
1✔
206
                                continue
1✔
207
                        }
208
                        dev.Status = stat
6✔
209
                        _, err = a.iothubClient.UpsertDevice(ctx, cs, twin.DeviceID, dev)
6✔
210
                        if err != nil {
8✔
211
                                err = errors.Wrap(err, "failed to update IoT Hub device identity")
2✔
212
                                if failEarly {
2✔
213
                                        return err
×
214
                                }
×
215
                                l.Error(err)
2✔
216
                        }
217
                }
218
        }
219

220
        // Find devices not present in IoT Hub
221
        for id, status := range statuses {
44✔
222
                if _, ok := devicesInHub[id]; !ok {
42✔
223
                        l.Warnf("Found device not existing in IoT Hub '%s': provisioning device", id)
3✔
224
                        // Device inconsistency
3✔
225
                        // Device exist in Mender but not in IoT Hub
3✔
226
                        err := a.provisionIoTHubDevice(ctx, id, integration, &iothub.Device{
3✔
227
                                DeviceID: id,
3✔
228
                                Status:   status,
3✔
229
                        })
3✔
230
                        if err != nil {
4✔
231
                                if failEarly {
1✔
232
                                        return err
×
233
                                }
×
234
                                l.Error(err)
1✔
235
                                continue
1✔
236
                        }
237
                }
238
        }
239
        return nil
5✔
240
}
241

242
func (a *app) GetDeviceStateIoTHub(
243
        ctx context.Context,
244
        deviceID string,
245
        integration *model.Integration,
246
) (*model.DeviceState, error) {
5✔
247
        cs := integration.Credentials.ConnectionString
5✔
248
        if cs == nil {
7✔
249
                return nil, ErrNoCredentials
2✔
250
        }
2✔
251
        twin, err := a.iothubClient.GetDeviceTwin(ctx, cs, deviceID)
3✔
252
        if err != nil {
4✔
253
                return nil, errors.Wrap(err, "failed to get the device twin")
1✔
254
        }
1✔
255
        return &model.DeviceState{
2✔
256
                Desired:  removeIoTHubMetadata(twin.Properties.Desired),
2✔
257
                Reported: removeIoTHubMetadata(twin.Properties.Reported),
2✔
258
        }, nil
2✔
259
}
260

261
func (a *app) SetDeviceStateIoTHub(
262
        ctx context.Context,
263
        deviceID string,
264
        integration *model.Integration,
265
        state *model.DeviceState,
266
) (*model.DeviceState, error) {
5✔
267
        cs := integration.Credentials.ConnectionString
5✔
268
        if cs == nil {
7✔
269
                return nil, ErrNoCredentials
2✔
270
        }
2✔
271
        twin, err := a.iothubClient.GetDeviceTwin(ctx, cs, deviceID)
3✔
272
        if err == nil {
6✔
273
                update := &iothub.DeviceTwinUpdate{
3✔
274
                        Tags: twin.Tags,
3✔
275
                        Properties: iothub.UpdateProperties{
3✔
276
                                Desired: state.Desired,
3✔
277
                        },
3✔
278
                        ETag:    twin.ETag,
3✔
279
                        Replace: true,
3✔
280
                }
3✔
281
                err = a.iothubClient.UpdateDeviceTwin(ctx, cs, deviceID, update)
3✔
282
        }
3✔
283
        if errHTTP, ok := err.(client.HTTPError); ok &&
3✔
284
                errHTTP.Code() == http.StatusPreconditionFailed {
4✔
285
                return nil, ErrDeviceStateConflict
1✔
286
        } else if err != nil {
4✔
287
                return nil, errors.Wrap(err, "failed to update the device twin")
1✔
288
        }
1✔
289
        return a.GetDeviceStateIoTHub(ctx, deviceID, integration)
1✔
290
}
291

292
func (app *app) VerifyDeviceTwin(ctx context.Context, req model.PreauthRequest) error {
8✔
293
        integrations, err := app.GetIntegrations(ctx)
8✔
294
        if err != nil {
9✔
295
                return fmt.Errorf("failed to retrieve integration: %w", err)
1✔
296
        }
1✔
297
        var integration model.Integration
7✔
298
        for _, integration = range integrations {
14✔
299
                if integration.Provider == model.ProviderIoTHub {
14✔
300
                        break
7✔
301
                }
302
        }
303
        deviceModule := strings.SplitN(req.DeviceID, "/", 2)
7✔
304
        for i := range deviceModule {
14✔
305
                deviceModule[i] = url.PathEscape(deviceModule[i])
7✔
306
        }
7✔
307
        id := strings.Join(deviceModule, "/modules/")
7✔
308
        log.FromContext(ctx).Debugf("getting twin: %s", id)
7✔
309
        twin, err := app.iothubClient.GetDeviceTwin(
7✔
310
                ctx, integration.Credentials.ConnectionString, id,
7✔
311
        )
7✔
312
        if err != nil {
8✔
313
                return fmt.Errorf("failed to get module twin from integration: %w", err)
1✔
314
        }
1✔
315
        idData, ok := twin.Properties.Reported["id_data"].(map[string]interface{})
6✔
316
        if !ok {
7✔
317
                return fmt.Errorf("missing identity data")
1✔
318
        }
1✔
319
        if !reflect.DeepEqual(idData, req.IdentityData) {
6✔
320
                return fmt.Errorf(`reported "id_data" does not match request`)
1✔
321
        }
1✔
322
        pubkeyPEM, ok := twin.Properties.Reported["pubkey"].(string)
4✔
323
        if !ok {
5✔
324
                return fmt.Errorf("missing pubkey")
1✔
325
        }
1✔
326
        var pubkey model.PublicKey
3✔
327
        err = pubkey.UnmarshalText([]byte(pubkeyPEM))
3✔
328
        if err != nil {
4✔
329
                return fmt.Errorf(
1✔
330
                        "invalid public key from twin: %w",
1✔
331
                        err,
1✔
332
                )
1✔
333
        }
1✔
334
        if !pubkey.PublicKey.Equal(req.PublicKey.PublicKey) {
3✔
335
                return fmt.Errorf("key does not match")
1✔
336
        }
1✔
337
        return nil
1✔
338
}
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