Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Common logic for device resources #207

Merged
merged 8 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions integration/util/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// Copyright (c) 2023 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0

package util

import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"strings"
)

const (
Indent = " "

deletedTemplate = "%s '%s' deleted\n"
done = "... done"

deviceJSON = `{"authorities":["auto-provisioning-enabled"]}`
k-gostev marked this conversation as resolved.
Show resolved Hide resolved

authJSON = `[{
"type": "hashed-password",
"auth-id": "%s",
"secrets": [{
"pwd-plain": "%s"
}]
}]`

thingJSON = `{"policyId": "%s"}`
k-gostev marked this conversation as resolved.
Show resolved Hide resolved

configDefaultMode = 0666

systemctl = "systemctl"
)

type Resource struct {
url string

method string
body string

user string
pass string

delete bool
}

type BootstrapConfiguration struct {
LogFile string `json:"logFile"`
PostBootstrapFile string `json:"postBootstrapFile"`
PostBootstrapScript []string `json:"postBootstrapScript"`
CaCert string `json:"caCert"`
Address string `json:"address"`
TenantID string `json:"tenantId"`
DeviceID string `json:"deviceId"`
AuthID string `json:"authId"`
Password string `json:"password"`
}

type ConnectorConfiguration struct {
CaCert string `json:"caCert"`
LogFile string `json:"logFile"`
Address string `json:"address"`
TenantID string `json:"tenantId"`
DeviceID string `json:"deviceId"`
AuthID string `json:"authId"`
Password string `json:"password"`
}

// CreateDeviceResources creates device resources.
func CreateDeviceResources(newDeviceId string, resources []*Resource,
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
tenantID, policyID, password, registryAPI, registryAPIUsername, registryAPIPassword string,
cfg *TestConfiguration) (*Resource, []*Resource) {

devicePath := tenantID + "/" + newDeviceId
deviceResource := &Resource{url: registryAPI + "/devices/" + devicePath, method: http.MethodPost,
body: deviceJSON, user: registryAPIUsername, pass: registryAPIPassword, delete: true}
resources = append(resources, deviceResource)

authID := strings.ReplaceAll(newDeviceId, ":", "_")
auth := fmt.Sprintf(authJSON, authID, password)
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
resources = append(resources, &Resource{url: registryAPI + "/credentials/" + devicePath, method: http.MethodPut,
body: auth, user: registryAPIUsername, pass: registryAPIPassword, delete: false})

thingURL := GetThingURL(cfg.DigitalTwinAPIAddress, newDeviceId)
thing := fmt.Sprintf(thingJSON, policyID)
resources = append(resources, &Resource{url: thingURL, method: http.MethodPut,
body: thing, user: cfg.DigitalTwinAPIUsername, pass: cfg.DigitalTwinAPIPassword, delete: true})
return deviceResource, resources
}

// WriteConfigFile writes interface data to the path file, creating it if necessary.
func WriteConfigFile(path string, cfg interface{}) error {
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
jsonContents, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
return fmt.Errorf("unable to marshal to json: %v", err)
}

// Preserve the file mode if the file already exists
mode := getFileModeOrDefault(path, configDefaultMode)
err = os.WriteFile(path, jsonContents, mode)
if err != nil {
return fmt.Errorf("unable to save file %s: %v", path, err)
}
return nil
}

func getFileModeOrDefault(path string, defaultMode os.FileMode) os.FileMode {
mode := defaultMode
fileInfo, err := os.Stat(path)
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
if err == nil {
mode = fileInfo.Mode()
}
return mode
}

// DeleteResources deletes all given resources and all related devices.
func DeleteResources(cfg *TestConfiguration, resources []*Resource, deviceId, url, user, pass string) bool {
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
ok := deleteRelatedDevices(cfg, deviceId, url, user, pass)
fmt.Println("deleting initially created things...")
// Delete in reverse order of creation
for i := len(resources) - 1; i >= 0; i-- {
r := resources[i]

if !r.delete {
continue
}

if _, err := SendDeviceRegistryRequest(nil, http.MethodDelete, r.url, r.user, r.pass); err != nil {
ok = false
fmt.Printf("%s unable to delete '%s', error: %v\n", Indent, r.url, err)
} else {
fmt.Printf(deletedTemplate, Indent, r.url)
}
}
return ok
}

func deleteRelatedDevices(cfg *TestConfiguration, viaDeviceID, url, user, pass string) bool {
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
devicesVia, ok := findDeviceRegistryDevicesVia(viaDeviceID, url, user, pass)
// Digital Twin API things are created after Device Registry devices, so delete them first
fmt.Println("deleting automatically created things...")
if !deleteDigitalTwinThings(cfg, devicesVia) {
ok = false
}
// Then delete Device Registry devices
fmt.Println("deleting automatically created devices...")
if !deleteRegistryDevices(devicesVia, url, user, pass) {
ok = false
}
return ok
}

func findDeviceRegistryDevicesVia(viaDeviceID, url, user, pass string) ([]string, bool) {
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
var devicesVia []string
ok := true

type registryDevice struct {
ID string `json:"id"`
Via []string `json:"via"`
}

type registryDevices struct {
Devices []*registryDevice `json:"result"`
}

contains := func(where []string, what string) bool {
for _, item := range where {
if item == what {
return true
}
}
return false
}

devicesJSON, err := SendDeviceRegistryRequest(nil, http.MethodGet, url, user, pass)
if err != nil {
ok = false
fmt.Printf("unable to list devices from the device registry, error: %v\n", err)
} else {
devices := &registryDevices{}
err = json.Unmarshal(devicesJSON, devices)
if err != nil {
ok = false
fmt.Printf("unable to parse devices JSON returned from the device registry, error: %v\n", err)
devices.Devices = nil
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
}
for _, device := range devices.Devices {
if contains(device.Via, viaDeviceID) {
devicesVia = append(devicesVia, device.ID)
}
}
}

return devicesVia, ok
}

func deleteDigitalTwinThings(cfg *TestConfiguration, things []string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return an error(any of them, if multiple) or pass testing.T as argument

ok := true
for _, thingID := range things {
url := GetThingURL(cfg.DigitalTwinAPIAddress, thingID)
_, err := SendDigitalTwinRequest(cfg, http.MethodDelete, url, nil)
if err != nil {
ok = false
fmt.Printf("error deleting thing: %v\n", err)
} else {
fmt.Printf(deletedTemplate, Indent, url)
}
}
return ok
}

func deleteRegistryDevices(devices []string, tenantURL, user, pass string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return an error(any of them, if multiple) or pass testing.T as argument

ok := true
for _, device := range devices {
url := tenantURL + device
if _, err := SendDeviceRegistryRequest(nil, http.MethodDelete, url, user, pass); err != nil {
ok = false
fmt.Printf("error deleting device: %v\n", err)
} else {
fmt.Printf(deletedTemplate, Indent, url)
}
}
return ok
}

// RestartService restarts the service with given name
func RestartService(service string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using single method with additional argument for restart and stop.

fmt.Printf("restarting %s...", service)
cmd := exec.Command(systemctl, "restart", service)
stdout, err := cmd.Output()
if err != nil {
fmt.Printf("error restarting %s: %v", service, err)
return false
}
fmt.Println(string(stdout))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider passing testing.T and logging the output

fmt.Println(done)
return true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return the error, not boolean

}

// StopService stops the service with given name
func StopService(service string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using single method with additional argument for restart and stop.

fmt.Printf("stopping %s...", service)
cmd := exec.Command(systemctl, "stop", service)
stdout, err := cmd.Output()
if err != nil {
fmt.Printf("error stopping %s: %v", service, err)
return false
}
fmt.Println(string(stdout))
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
fmt.Println(done)
return true
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
}

// CopyFile copies source file to the destination.
func CopyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
// If the destination file exists, preserve its file mode.
// If the destination file doesn't exist, use the file mode of the source file.
srcMode := getFileModeOrDefault(src, configDefaultMode)
dstMode := getFileModeOrDefault(dst, srcMode)
return os.WriteFile(dst, data, dstMode)
}

// DeleteFile removes the named file or directory.
func DeleteFile(path string) bool {
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
if err := os.Remove(path); err != nil {
fmt.Printf("unable to delete file %s, error: %v", path, err)
return false
}
return true
}
29 changes: 26 additions & 3 deletions integration/util/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,25 @@ const (
StopSendMessages UnsubscribeEventType = "STOP-SEND-MESSAGES"
)

// SendDigitalTwinRequest sends а new HTTP request to the Ditto REST API
// SendDigitalTwinRequest sends a new HTTP request to the Ditto REST API
func SendDigitalTwinRequest(cfg *TestConfiguration, method string, url string, body interface{}) ([]byte, error) {
req, err := createRequest(make([]byte, 0), body, method, url, cfg.DigitalTwinAPIUsername, cfg.DigitalTwinAPIPassword)
if err != nil {
return nil, err
}
return sendRequest(req, method, url)
}

// SendDeviceRegistryRequest sends a new HTTP request to the Ditto API
func SendDeviceRegistryRequest(payload []byte, method string, url string, username string, password string) ([]byte, error) {
req, err := createRequest(payload, nil, method, url, username, password)
if err != nil {
return nil, err
}
return sendRequest(req, method, url)
}

func createRequest(payload []byte, body interface{}, method, url, username, password string) (*http.Request, error) {
k-gostev marked this conversation as resolved.
Show resolved Hide resolved
var reqBody io.Reader

if body != nil {
Expand All @@ -70,21 +87,27 @@ func SendDigitalTwinRequest(cfg *TestConfiguration, method string, url string, b
return nil, err
}
reqBody = bytes.NewBuffer(jsonValue)
} else {
reqBody = bytes.NewBuffer(payload)
}

req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, err
}

req.Header.Add("Content-Type", "application/json")
if body != nil {
correlationID := uuid.New().String()
req.Header.Add("Content-Type", "application/json")
req.Header.Add("correlation-id", correlationID)
req.Header.Add("response-required", "true")
}

req.SetBasicAuth(cfg.DigitalTwinAPIUsername, cfg.DigitalTwinAPIPassword)
req.SetBasicAuth(username, password)
return req, nil
}

func sendRequest(req *http.Request, method string, url string) ([]byte, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
Expand Down