-
Notifications
You must be signed in to change notification settings - Fork 0
/
requests.go
231 lines (210 loc) · 8.28 KB
/
requests.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package hardware
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/smarthome-go/smarthome/core/database"
"github.com/smarthome-go/smarthome/core/event"
)
type PowerRequest struct {
Switch string `json:"switch"`
Power bool `json:"power"`
}
// Checks if a node is online and updates the database entry accordingly
func checkNodeOnlineRequest(node database.HardwareNode) error {
// Client has timeout of 5 seconds in order to keep wait time short
client := http.Client{Timeout: time.Second * 5}
// Build URL
urlTemp, err := url.Parse(node.Url)
if err != nil {
log.Warn(fmt.Sprintf(
"Can not check health of node: '%s' due to malformed base URL: %s",
node.Name,
err.Error(),
))
return err
}
urlTemp.Path = "/health"
// Perform the health-check request
res, err := client.Get(urlTemp.String())
if err != nil {
log.Error("Hardware node checking request failed: ", err.Error())
return err
}
if res.StatusCode != 200 {
log.Error(fmt.Sprintf("Hardware node checking request failed: received status code %d (%s)", res.StatusCode, res.Status))
return fmt.Errorf("Hardware node checking request failed: received status code %d (%s)", res.StatusCode, res.Status)
}
return nil
}
// Runs the check request and updated the database entry accordingly
func checkNodeOnline(node database.HardwareNode) error {
if err := checkNodeOnlineRequest(node); err != nil {
// `node.Online` is checked to be `true` because it is still the value before the healthcheck
if node.Online {
log.Warn(fmt.Sprintf("Node '%s' failed to respond and is now offline", node.Name))
go event.Error("Node Offline",
fmt.Sprintf("Node '%s' went offline. It is recommended to address this issue as soon as possible", node.Name),
)
}
if errDB := database.SetNodeOnline(node.Url, false); errDB != nil {
log.Error("Failed to update power state of node: ", errDB.Error())
return errDB
}
return nil
}
// `!node.Online` is checked because it is still the value before the healthcheck
if !node.Online {
log.Info(fmt.Sprintf("Node '%s' is back online", node.Name))
go event.Info("Node back online", fmt.Sprintf("Node '%s' is back online.", node.Name))
}
if errDB := database.SetNodeOnline(node.Url, true); errDB != nil {
log.Error("Failed to update power state of node: ", errDB.Error())
return errDB
}
return nil
}
// Dispatches a power job to a given hardware node
// Returns an error if the job fails to execute on the hardware
// However, the preferred method of communication is by using the API `SetPower()` this way, priorities and interrupts are scheduled automatically
// A check if a node is online again can be still executed afterwards
func sendPowerRequest(node database.HardwareNode, switchName string, powerOn bool) error {
// Create the request body for the request
requestBody, err := json.Marshal(PowerRequest{
Switch: switchName,
Power: powerOn,
})
if err != nil {
log.Error("Could not encode node request body: ", err.Error())
return err
}
// Creates a client with a timeout of 2 seconds
// TODO: make timeout editable / decide to use better timeout
client := http.Client{Timeout: time.Second * 2}
// Build a URL using the node parameters
urlTemp, err := url.Parse(node.Url)
if err != nil {
log.Warn(fmt.Sprintf("Can not send power request to node '%s' due to malformed URL: %s", node.Name, err.Error()))
}
urlTemp.Path = "/power"
// Build the token query
query := url.Values{}
query.Add("token", node.Token)
urlTemp.RawQuery = query.Encode()
// Perform the request
res, err := client.Post(
urlTemp.String(),
"application/json",
bytes.NewBuffer(requestBody),
)
if err != nil {
log.Error("Hardware node request failed: ", err.Error())
return err
}
// Evaluate non-200 outcome
if res.StatusCode != 200 {
// TODO: check firmware version of the hardware nodes at startup / in the healthcheck
switch res.StatusCode {
case 400:
log.Error(fmt.Sprintf("Power request to node '%s' failed with code '400/bad-request': smarthome has sent a request that the node could not process", node.Name))
case 401:
log.Error(fmt.Sprintf("Power request to node '%s' failed with code '401/unauthorized': token configuration is likely invalid", node.Name))
case 422:
log.Error(fmt.Sprintf("Power request to node '%s' failed with code 422/unprocessable-entity: the requested switch is not configured on the node", node.Name))
case 423:
log.Error(fmt.Sprintf("Power request to node '%s' failed with code 423/locked: node is currently in use by another service", node.Name))
case 500:
log.Error(fmt.Sprintf("Power request to node '%s' failed with code 500/internal-server-error: undefined error which could not be matched", node.Name))
case 503:
log.Error(fmt.Sprintf("Power request to node '%s' failed with code 503/service-unavailable: node is currently in maintenance mode", node.Name))
default:
log.Error(fmt.Sprintf("Power request to node '%s' failed with unknown status code: %s", node.Name, res.Status))
}
return errors.New("set power failed: non 200 status code")
}
return nil
}
// A wrapper function which calls `checkNodeOnline`
// Does not return errors, just prints them
// Is used for determining whether a node which was previously offline is back online
// Is used via a `go` call for lower latency
func checkNodeOnlineWrapper(node database.HardwareNode) {
if err := checkNodeOnline(node); err != nil {
log.Trace(fmt.Sprintf("Node: '%s' is still offline", node.Name))
}
}
// More user-friendly API to directly address all hardware nodes
// However, the preferred method of communication is by using the API `ExecuteJob()` this way, priorities and interrupts are scheduled automatically
// This function is internally used by `ExecuteJob`
// Makes a database request at the beginning in order to obtain information about the available nodes
// Updates the power state in the database after the jobs have been sent to the hardware nodes
func setPowerOnAllNodes(switchName string, powerOn bool) error {
var err error
// Retrieves available hardware nodes from the database
nodes, err := database.GetHardwareNodes()
if err != nil {
log.Error("Failed to process power request: could not get nodes from database: ", err.Error())
return err
}
for _, node := range nodes {
if !node.Online && node.Enabled {
// Check if the node is back online
// a goroutine is used in order to keep up a fast response time
go checkNodeOnlineWrapper(node)
log.Warn(fmt.Sprintf("Skipping node: '%s' because it is currently marked as offline", node.Name))
continue
}
// If the node is not enabled, skip the request
if !node.Enabled {
log.Trace(fmt.Sprintf("Skipping power request to disabled node '%s'", node.Name))
continue
}
// Perform node request
errTemp := sendPowerRequest(node, switchName, powerOn)
if errTemp != nil {
event.Error("Node Request Failed", fmt.Sprintf("Power request to node '%s' failed: %s", node.Name, errTemp.Error()))
// If the request failed, check the node and mark it as offline
if err := checkNodeOnline(node); err != nil {
log.Error("Failed to check node online: ", err.Error())
}
err = errTemp
} else {
// If the node was previously offline and is now online, run a healthcheck to update its state
// Log the event and mark the node as `online`
if !node.Online {
if err := checkNodeOnline(node); err != nil {
log.Error("Failed to check node online: ", err.Error())
return err
}
}
log.Debug("Successfully dispatched power request to: ", node.Name)
}
}
// Update the switch power-state in the database
if _, err := database.SetPowerState(switchName, powerOn); err != nil {
log.Error("Failed to set power after dispatching to all nodes: updating power-state database entry failed: ", err.Error())
return err
}
return err
}
// Runs a health-check on all nodes of the system
// Used in system-level healthcheck
func RunNodeCheck() error {
log.Debug("Running hardware node health check...")
nodes, err := database.GetHardwareNodes()
if err != nil {
log.Error("Failed to check all hardware nodes: ", err.Error())
}
for _, node := range nodes {
if err := checkNodeOnline(node); err != nil {
log.Error(fmt.Sprintf("Healthcheck of node '%s' failed: %s", node.Name, err.Error()))
return nil
}
}
log.Debug("Hardware node healtheck finished")
return nil
}