-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsnipe-agent.go
362 lines (327 loc) · 11.6 KB
/
snipe-agent.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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
package main
// Import all the things
import (
"fmt"
"time"
"encoding/json"
"os"
"os/exec"
"net/http"
"net/url"
"io/ioutil"
"strings"
"flag"
"runtime"
"net"
"log"
"bytes"
)
// Set all the variables.
var NetworkHost string = "https://google.com" //website to test if network is up and to get preferred local IP address.
var SnipeHost string = ""
var SnipeKey string = ""
var UpdateFrequency = 15 // In Minutes
var SnipeID = 0 // This should always be set to 0 as snipe-agent will change it once the SN lookup succeeds.
var BuildVersion string = "v0.1.0"
var StatusID = 1 // The Snipe Status ID that assets running the agent should be set to.
// Define all the Structs
// SnipeResults is a base structure for parsing returned search results.
type SnipeResults struct{
Total int `json:"total"`
AssetList []AssetProfile `json:"rows"`
}
// AssetProfile is an object style representation of returned Snipe-IT data.
type AssetProfile struct{
Id int `json:"id"`
Name string `json:"name"`
AssetTag string `json:"asset_tag"`
Serial string `json:"serial"`
Notes string `json:"notes"`
}
// SnipeUpdatePayload is an object style that represents the patch Snipe-IT data.
type SnipeUpdatePayload struct{
Name string `json:"name"`
Status_id int `json:"status_id"`
}
// Create Functions
func GetExternalIP() string {
switch os := runtime.GOOS; os {
case "windows":
// Powershell invoke Rest Method to get the IP.
cmd := exec.Command("powershell", "Invoke-RestMethod", "http://ipinfo.io/json", "|", "Select", "-exp", "ip")
result, err := cmd.Output()
if err != nil {
fmt.Println(err)
// Return err on error so we don't try to update.
return "err"
}
// The result, by default contains carriage returns we need to remove:
return strings.Replace(string(result), "\r\n", "", -1)
// Linux command for the external IP.
case "linux":
// Powershell invoke Rest Method to get the IP.
cmd := exec.Command("dig", "@resolver1.opendns.com", "ANY", "myip.opendns.com", "+short")
result, err := cmd.Output()
if err != nil {
fmt.Println(err)
// Return err on error so we don't try to update.
return "err"
}
// The result, by default contains carriage returns we need to remove:
return strings.Replace(string(result), "\r\n", "", -1)
// macOS
case "darwin":
// Powershell invoke Rest Method to get the IP.
cmd := exec.Command("dig", "@resolver1.opendns.com", "ANY", "myip.opendns.com", "+short")
result, err := cmd.Output()
if err != nil {
fmt.Println(err)
// Return err on error so we don't try to update.
return "err"
}
// The result, by default contains carriage returns we need to remove:
return strings.Replace(string(result), "\r\n", "", -1)
case "default":
fmt.Print("OS not Supported")
return "err"
}
return "err"
}
func GetSerialNumber() string {
switch os := runtime.GOOS; os {
case "windows":
// Run a powershell command to grab the Serial Number
cmd := exec.Command("powershell", "gwmi", "win32_bios", "|", "Select-Object", "-ExpandProperty", "SerialNumber")
sn, err := cmd.Output()
if err != nil {
fmt.Println(err)
// Return err on error so we don't try to update.
return "err"
}
// The serial number is byte encoded so when we return it, convert it to a string.
return string(sn)
default:
fmt.Print("OS not Supported")
return "err"
}
}
// universal get hostname
func GetHostName() string{
name, err := os.Hostname()
if err != nil {
fmt.Println(err)
return "err"
}
return string(name)
}
// Get preferred outbound ip of this machine
func GetPreferredLocalIP() string {
var ConnectionString string = ""
if strings.HasPrefix(NetworkHost, "https://"){
//remove the https prefix and form the string for the connection
ConnectionString = strings.Replace(NetworkHost, "https://", "", -1) + ":https"
}
if strings.HasPrefix(NetworkHost, "http://"){
//remove the http prefix and form the string for the connection
ConnectionString = strings.Replace(NetworkHost, "http://", "", -1) + ":http"
}
conn, err := net.Dial("tcp", ConnectionString)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.TCPAddr)
return localAddr.IP.String()
}
func GetCurrentUser() string {
switch os := runtime.GOOS; os {
// Make a case for Windows computers.
case "windows":
// Run a powershell command to grab the Serial Number
cmd := exec.Command("powershell", "gwmi", "win32_process", "-f", `'Name="explorer.exe"'`, "|", "%", "getowner", "|", "%", "user" )
result, err := cmd.Output()
if err != nil {
fmt.Println(err)
// Return err on error so we don't try to update.
return "err"
}
// The result, by default contains carriage returns we need to remove:
return strings.Replace(string(result), "\r\n", "", -1)
// Make a case for macOS computers.
case "darwin":
// Run a command to get the username that currently owns the console.
cmd:= exec.Command("stat", "-f", "'%Su'", "/dev/console" )
result, err := cmd.Output()
if err != nil {
fmt.Println(err)
// Return err on error so we don't try to update.
return "err"
}
// The result, by default contains carriage returns we need to remove:
return strings.Replace(string(result), "\r\n", "", -1)
// There is no case for the remaining OS types, so return an error.
default:
fmt.Print("OS not Supported")
return "err"
}
}
func FindSnipeID() int {
// Get the SerialNumber so we can perform a search in Snipe-IT
SerialNumber := GetSerialNumber()
if SerialNumber == "err"{
// We've Errored, so return 0
return 0
}
// Perform lookup based off SN
// Create the Web address we need.
EncodedSN := &url.URL{Path: SerialNumber}
// Remove Carriage and newLine Returns from encoded string.
FixedEncodedSN := strings.Replace(EncodedSN.String(), "%0D%0A", "", -1)
web := SnipeHost + "/api/v1/hardware/byserial/" + FixedEncodedSN
// Set up the request and headers.
req, err := http.NewRequest("GET", web, nil)
req.Header.Add("Authorization", "Bearer " + SnipeKey)
req.Header.Add("Accept", "application/json")
// Send the request with http client
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
fmt.Println("Got an error:")
fmt.Println(err)
// We failed, so return 0
return 0
}
if response.StatusCode != 200 {
// We didn't get a 200 response so return a failure.
fmt.Println("Received an invalid response: ", response.StatusCode)
// We failed, so return 0
return 0
}
body, _ := ioutil.ReadAll(response.Body)
var sniperesults SnipeResults
json.Unmarshal(body,&sniperesults)
if sniperesults.Total != 1 {
// We got too many or few results, so return 0 for our loop.
return 0
}
// We don't need to check for a null value, since INT returns 0 in that case. So just shipit.
return sniperesults.AssetList[0].Id
}
// Recieves updated payload and ships it to snipe
func PatchToSnipe(assetPayload SnipeUpdatePayload) bool{
// Create he URI to patch the asset.
web := fmt.Sprintf("%s/api/v1/hardware/%d", SnipeHost, SnipeID)
// Marshal updated payload becuase api call needs a marshalled json
jsonPayload, err := json.Marshal(assetPayload)
if err != nil {
fmt.Println("Got an error:")
fmt.Println(err)
// We failed, so return false
return false
}
// Create the body from the JSON.
body := []byte(string(jsonPayload))
// Set up the request and headers.
req, err := http.NewRequest("PATCH", web, bytes.NewBuffer(body))
if err != nil {
fmt.Println("Got an error:")
fmt.Println(err)
// We failed, so return false
return false
}
req.Header.Add("Authorization", "Bearer " + SnipeKey)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-type", "application/json")
// Send the request with http client
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
fmt.Println("Got an error:")
fmt.Println(err)
// We failed, so return false
return false
}
defer response.Body.Close()
if response.StatusCode != 200 {
// We didn't get a 200 response so return a failure.
fmt.Println("Received an invalid response: ", response.StatusCode)
fmt.Println("Response Body: ", response.Body)
// We failed, so return false
return false
}
return true
}
// Recieves a struct and returns a updated struct
func PopulatePayload(assetInfo SnipeUpdatePayload) SnipeUpdatePayload{
assetInfo.Name = GetHostName()
assetInfo.Status_id = StatusID
return assetInfo
}
func CheckWebHost(web string) bool{
response, errors := http.Get(web)
if errors != nil {
// Fail, because there was an error.
return false
}
if response.StatusCode == 200 {
return true
}
// We didn't get a 200 response so return a failure.
return false
}
// Main
func main() {
// Set up Runtime flags.
version := flag.Bool("version", false, "Report the version number and quit.")
flag.Parse()
// If the --version flag is present, report the BuildVersion and exit.
if *version {
fmt.Println(BuildVersion)
os.Exit(0)
}
// Since this will be a service, set up a loop that repeats over an interval
for {
// Run some checks.
// Try to connect to a stable network server.
if CheckWebHost(NetworkHost) != true {
fmt.Printf("%s could not be reached. Will retry at the next checkin interval.\n", NetworkHost)
time.Sleep( time.Duration(UpdateFrequency) * time.Minute)
// Continue so we don't run any more code and start the loop over again.
continue
}
fmt.Println("Network seems up.")
// Try to contact SnipeHost - Cycle on a failure with UpdateFrequency.
if CheckWebHost(SnipeHost) != true {
fmt.Printf("%s could not be reached. Will retry at the next checkin interval.\n", SnipeHost)
time.Sleep( time.Duration(UpdateFrequency) * time.Minute)
// Continue so we don't run any more code and start the loop over again.
continue
}
fmt.Println("Snipe-IT instance seems up.")
// If we don't know the SnipeID, look it up. - This way limits the number of API calls we need to make.
if SnipeID < 1 {
SnipeID = FindSnipeID()
if SnipeID < 1 {
fmt.Printf("The Snipe ID could not be found. Will retry at the next checkin interval.\n")
time.Sleep( time.Duration(UpdateFrequency) * time.Minute)
// Continue so we don't run any more code and start the loop over again.
continue
}
}
// Create a new blank SnipeUpdatePayload
var assetInfo SnipeUpdatePayload
// Pass blank payload into populate function which returns the current data.
assetPayload := PopulatePayload(assetInfo)
// Pass filled struct into function to update snipe
// Set the returned value to update so we can quit if there was an error.
update := PatchToSnipe(assetPayload)
// Check if the update failed or not so we can fail the service if needed.
if update != true {
// We've failed, raise a system exit.
fmt.Println("Patching the snipe asset failed, which shouldn't have occured. There is likely something wrong with your build. Exiting.")
os.Exit(1)
}
// Sleep interval to delay updating again
time.Sleep( time.Duration(UpdateFrequency) * time.Minute)
}
}