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  }