github.com/simpleiot/simpleiot@v0.18.3/cmd/siot/main.go (about) 1 // This is the main Simple IoT Program 2 package main 3 4 import ( 5 "context" 6 "errors" 7 "flag" 8 "fmt" 9 "io" 10 "log" 11 "os" 12 "os/exec" 13 "os/user" 14 "path" 15 "runtime" 16 "strings" 17 "syscall" 18 "text/template" 19 "time" 20 21 "github.com/oklog/run" 22 "github.com/simpleiot/simpleiot/client" 23 "github.com/simpleiot/simpleiot/install" 24 "github.com/simpleiot/simpleiot/server" 25 ) 26 27 // goreleaser will replace version with Git version. You can also pass version 28 // into the version into the go build: 29 // 30 // go build -ldflags="-X main.version=1.2.3" 31 var version = "Development" 32 33 func main() { 34 // global options 35 flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 36 flagVersion := flags.Bool("version", false, "Print app version") 37 flagID := flags.String("id", "", "ID for the instance") 38 flags.Usage = func() { 39 fmt.Println("usage: siot [OPTION]... COMMAND [OPTION]...") 40 fmt.Println("Global options:") 41 flags.PrintDefaults() 42 fmt.Println() 43 fmt.Println("Available commands:") 44 fmt.Println(" - serve (start the SIOT server)") 45 fmt.Println(" - log (log SIOT messages)") 46 fmt.Println(" - store (store maint, requires server to be running)") 47 fmt.Println(" - install (install SIOT and register service)") 48 fmt.Println(" - import (import nodes from YAML file)") 49 fmt.Println(" - export (export nodes to YAML file)") 50 } 51 52 _ = flags.Parse(os.Args[1:]) 53 54 if *flagVersion { 55 fmt.Println(version) 56 os.Exit(0) 57 } 58 59 log.Printf("SimpleIOT %v\n", version) 60 61 // extract sub command and its arguments 62 args := flags.Args() 63 64 if len(args) < 1 { 65 // gun serve command by default 66 args = []string{"serve"} 67 } 68 69 switch args[0] { 70 case "serve": 71 if err := runServer(args[1:], version, *flagID); err != nil { 72 log.Println("Simple IoT stopped, reason:", err) 73 } 74 case "log": 75 runLog(args[1:]) 76 case "store": 77 runStore(args[1:]) 78 case "install": 79 runInstall(args[1:]) 80 case "import": 81 runImport(args[1:]) 82 case "export": 83 runExport(args[1:]) 84 default: 85 log.Fatal("Unknown command; options: serve, log, store") 86 } 87 } 88 89 func runServer(args []string, version string, id string) error { 90 flags := flag.NewFlagSet("serve", flag.ExitOnError) 91 options, err := server.Args(args, flags) 92 if err != nil { 93 return err 94 } 95 96 options.AppVersion = version 97 options.ID = id 98 99 if options.LogNats { 100 client.Log(options.NatsServer, options.AuthToken) 101 select {} 102 } 103 104 var g run.Group 105 106 siot, nc, err := server.NewServer(options) 107 108 if err != nil { 109 siot.Stop(nil) 110 return fmt.Errorf("Error starting server: %v", err) 111 } 112 113 g.Add(siot.Run, siot.Stop) 114 115 g.Add(run.SignalHandler(context.Background(), 116 syscall.SIGINT, syscall.SIGTERM)) 117 118 // Load the default SIOT clients -- you can replace this with a customized 119 // list 120 clients, err := client.DefaultClients(nc) 121 if err != nil { 122 return err 123 } 124 siot.AddClient(clients) 125 126 ctx, cancel := context.WithTimeout(context.Background(), time.Second*9) 127 128 // add check to make sure server started 129 chStartCheck := make(chan struct{}) 130 g.Add(func() error { 131 err := siot.WaitStart(ctx) 132 if err != nil { 133 return errors.New("Timeout waiting for SIOT to start") 134 } 135 log.Println("SIOT started") 136 137 <-chStartCheck 138 return nil 139 }, func(_ error) { 140 cancel() 141 close(chStartCheck) 142 }) 143 144 return g.Run() 145 } 146 147 var defaultNatsServer = "nats://127.0.0.1:4222" 148 149 func runLog(args []string) { 150 flags := flag.NewFlagSet("log", flag.ExitOnError) 151 flagNatsServer := flags.String("natsServer", defaultNatsServer, "NATS Server") 152 flagAuthToken := flags.String("token", "", "Auth token") 153 154 if err := flags.Parse(args); err != nil { 155 log.Fatal("error: ", err) 156 } 157 158 // only consider env if command line option is something different 159 // that default 160 natsServer := *flagNatsServer 161 if natsServer == defaultNatsServer { 162 natsServerE := os.Getenv("SIOT_NATS_SERVER") 163 if natsServerE != "" { 164 natsServer = natsServerE 165 } 166 } 167 168 authToken := *flagAuthToken 169 if authToken == "" { 170 authTokenE := os.Getenv("SIOT_AUTH_TOKEN") 171 if authTokenE != "" { 172 authToken = authTokenE 173 } 174 } 175 176 client.Log(natsServer, authToken) 177 178 select {} 179 } 180 181 func runStore(args []string) { 182 flags := flag.NewFlagSet("store", flag.ExitOnError) 183 flagNatsServer := flags.String("natsServer", defaultNatsServer, "NATS Server") 184 flagAuthToken := flags.String("token", "", "Auth token") 185 flagCheck := flags.Bool("check", false, "Check store") 186 flagFix := flags.Bool("fix", false, "Fix store") 187 188 if err := flags.Parse(args); err != nil { 189 log.Fatal("error: ", err) 190 } 191 192 // only consider env if command line option is something different 193 // that default 194 natsServer := *flagNatsServer 195 if natsServer == defaultNatsServer { 196 natsServerE := os.Getenv("SIOT_NATS_SERVER") 197 if natsServerE != "" { 198 natsServer = natsServerE 199 } 200 } 201 202 authToken := *flagAuthToken 203 if authToken == "" { 204 authTokenE := os.Getenv("SIOT_AUTH_TOKEN") 205 if authTokenE != "" { 206 authToken = authTokenE 207 } 208 } 209 210 opts := client.EdgeOptions{ 211 URI: natsServer, 212 AuthToken: authToken, 213 NoEcho: true, 214 Disconnected: func() { 215 log.Println("NATS Disconnected") 216 }, 217 Reconnected: func() { 218 log.Println("NATS Reconnected") 219 }, 220 Closed: func() { 221 log.Println("NATS Closed") 222 os.Exit(0) 223 }, 224 Connected: func() { 225 log.Println("NATS Connected") 226 }, 227 } 228 229 nc, err := client.EdgeConnect(opts) 230 231 if err != nil { 232 log.Println("Error connecting to NATS server:", err) 233 os.Exit(-1) 234 } 235 236 switch { 237 case *flagCheck: 238 err := client.AdminStoreVerify(nc) 239 if err != nil { 240 log.Println("DB verify failed:", err) 241 } else { 242 log.Println("DB verified :-)") 243 } 244 245 case *flagFix: 246 err := client.AdminStoreMaint(nc) 247 if err != nil { 248 log.Println("DB maint failed:", err) 249 } else { 250 log.Println("DB maint success :-)") 251 } 252 253 default: 254 fmt.Println("Error, no operation given.") 255 flags.Usage() 256 } 257 } 258 259 func runCommand(cmd string) (string, error) { 260 c := exec.Command("sh", "-c", cmd) 261 ret, err := c.CombinedOutput() 262 return string(ret), err 263 } 264 265 type serviceData struct { 266 SiotData string 267 SiotPath string 268 SystemdTarget string 269 } 270 271 func runInstall(args []string) { 272 flags := flag.NewFlagSet("install", flag.ExitOnError) 273 274 if err := flags.Parse(args); err != nil { 275 log.Fatal("error: ", err) 276 } 277 278 if runtime.GOOS != "linux" { 279 log.Fatal("Install is only supported on Linux systems") 280 } 281 282 currentUser, err := user.Current() 283 if err != nil { 284 log.Fatal("Error getting user: ", err) 285 } 286 287 isRoot := false 288 if currentUser.Username == "root" { 289 isRoot = true 290 } 291 292 serviceDir := path.Join(currentUser.HomeDir, ".config/systemd/user") 293 dataDir := path.Join(currentUser.HomeDir, ".local/share/siot") 294 295 if isRoot { 296 serviceDir = path.Join("/etc/systemd/system") 297 dataDir = "/var/lib/siot" 298 } 299 300 mkdirs := []string{serviceDir, dataDir} 301 302 for _, d := range mkdirs { 303 err := os.MkdirAll(d, 0755) 304 if err != nil { 305 log.Fatalf("Error creating dir %v: %v\n", d, err) 306 } 307 } 308 309 servicePath := path.Join(serviceDir, "siot.service") 310 311 siotPath, err := os.Executable() 312 if err != nil { 313 log.Fatal("Error getting SIOT path: ", err) 314 } 315 316 log.Println("Installing service file:", servicePath) 317 log.Println("SIOT executable location:", siotPath) 318 log.Println("SIOT data location:", dataDir) 319 320 _, err = os.Stat(servicePath) 321 322 if err == nil { 323 log.Println("Service file exists, do you want to replace it? (yes/no)") 324 325 var input string 326 327 _, err := fmt.Scan(&input) 328 if err != nil { 329 log.Fatal("Error getting input: ", err) 330 } 331 332 input = strings.ToLower(input) 333 334 if input != "yes" { 335 log.Fatal("Exitting install") 336 } 337 } 338 339 siotService, err := install.Content.ReadFile("siot.service") 340 if err != nil { 341 log.Fatal("Error reading embedded service file: ", err) 342 } 343 344 t, err := template.New("service").Parse(string(siotService)) 345 if err != nil { 346 log.Fatal("Error parsing service template", err) 347 } 348 349 serviceOut, err := os.Create(servicePath) 350 if err != nil { 351 log.Fatal("Error creating service file: ", err) 352 } 353 354 sd := serviceData{ 355 SiotPath: siotPath, 356 SiotData: dataDir, 357 SystemdTarget: "default.target", 358 } 359 360 if isRoot { 361 sd.SystemdTarget = "multi-user.target" 362 } 363 364 err = t.Execute(serviceOut, sd) 365 366 if err != nil { 367 log.Fatal("Error installing service file: ", err) 368 } 369 370 // start and enable service 371 startCmd := "systemctl start siot" 372 enableCmd := "systemctl enable siot" 373 reloadCmd := "systemctl daemon-reload" 374 375 if !isRoot { 376 startCmd += " --user" 377 enableCmd += " --user" 378 reloadCmd += " --user" 379 } 380 381 cmds := []string{startCmd, enableCmd, reloadCmd} 382 383 for _, c := range cmds { 384 _, err := runCommand(c) 385 if err != nil { 386 log.Fatalf("Error running command: %v: %v\n", c, err) 387 } 388 } 389 390 log.Println("Install success!") 391 log.Println("Please update ports in service file if you want someting other than defaults") 392 } 393 394 func runImport(args []string) { 395 flags := flag.NewFlagSet("import", flag.ExitOnError) 396 397 flagParentID := flags.String("parentID", "", "Parent ID for import under. Use \"root\" for complete restore") 398 flagNatsServer := flags.String("natsServer", defaultNatsServer, "NATS Server") 399 flagAuthToken := flags.String("token", "", "Auth token") 400 flagPreserveIDs := flags.Bool("preserveIDs", false, "Preserve node IDs (use with caution)") 401 402 if err := flags.Parse(args); err != nil { 403 log.Fatal("error: ", err) 404 } 405 406 // only consider env if command line option is something different 407 // that default 408 natsServer := *flagNatsServer 409 if natsServer == defaultNatsServer { 410 natsServerE := os.Getenv("SIOT_NATS_SERVER") 411 if natsServerE != "" { 412 natsServer = natsServerE 413 } 414 } 415 416 authToken := *flagAuthToken 417 if authToken == "" { 418 authTokenE := os.Getenv("SIOT_AUTH_TOKEN") 419 if authTokenE != "" { 420 authToken = authTokenE 421 } 422 } 423 424 opts := client.EdgeOptions{ 425 URI: natsServer, 426 AuthToken: authToken, 427 NoEcho: true, 428 Disconnected: func() { 429 log.Println("NATS Disconnected") 430 }, 431 Reconnected: func() { 432 log.Println("NATS Reconnected") 433 }, 434 Closed: func() { 435 log.Fatal("NATS Closed") 436 }, 437 Connected: func() { 438 log.Println("NATS Connected") 439 }, 440 } 441 442 nc, err := client.EdgeConnect(opts) 443 if err != nil { 444 log.Fatal("Error connecting to NATS server: ", err) 445 } 446 447 yamlChan := make(chan []byte) 448 449 go func() { 450 // read YAML file from STDIN 451 yaml, err := io.ReadAll(os.Stdin) 452 if err != nil { 453 log.Fatal("Error reading YAML from stdin: ", err) 454 } 455 yamlChan <- yaml 456 }() 457 458 var yaml []byte 459 460 select { 461 case yaml = <-yamlChan: 462 case <-time.After(time.Second * 2): 463 log.Fatal("Error: timeout reading YAML from STDIN") 464 } 465 466 err = client.ImportNodes(nc, *flagParentID, yaml, "import", *flagPreserveIDs) 467 if err != nil { 468 log.Fatal("Error importing nodes: ", err) 469 } 470 471 log.Println("Import success!") 472 } 473 474 func runExport(args []string) { 475 flags := flag.NewFlagSet("import", flag.ExitOnError) 476 477 flagNodeID := flags.String("nodeID", "", "node ID to export. Default is root device") 478 flagNatsServer := flags.String("natsServer", defaultNatsServer, "NATS Server") 479 flagAuthToken := flags.String("token", "", "Auth token") 480 481 if err := flags.Parse(args); err != nil { 482 log.Fatal("error: ", err) 483 } 484 485 // only consider env if command line option is something different 486 // that default 487 natsServer := *flagNatsServer 488 if natsServer == defaultNatsServer { 489 natsServerE := os.Getenv("SIOT_NATS_SERVER") 490 if natsServerE != "" { 491 natsServer = natsServerE 492 } 493 } 494 495 authToken := *flagAuthToken 496 if authToken == "" { 497 authTokenE := os.Getenv("SIOT_AUTH_TOKEN") 498 if authTokenE != "" { 499 authToken = authTokenE 500 } 501 } 502 503 opts := client.EdgeOptions{ 504 URI: natsServer, 505 AuthToken: authToken, 506 NoEcho: true, 507 Disconnected: func() { 508 log.Println("NATS Disconnected") 509 }, 510 Reconnected: func() { 511 log.Println("NATS Reconnected") 512 }, 513 Closed: func() { 514 log.Fatal("NATS Closed") 515 }, 516 Connected: func() { 517 log.Println("NATS Connected") 518 }, 519 } 520 521 nc, err := client.EdgeConnect(opts) 522 if err != nil { 523 log.Fatal("Error connecting to NATS server: ", err) 524 } 525 526 yaml, err := client.ExportNodes(nc, *flagNodeID) 527 if err != nil { 528 log.Fatal("Error export nodes: ", err) 529 } 530 531 _, err = os.Stdout.Write(yaml) 532 533 if err != nil { 534 log.Fatal("Error writing YAML to STDOUT: ", err) 535 } 536 537 }