diff options
author | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-05-11 19:42:00 +0200 |
---|---|---|
committer | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-05-11 19:42:22 +0200 |
commit | ec7051e1da665206f594c7616ad381bfeaea333a (patch) | |
tree | ecc01ee358794c9d8c5fbb87d2f5b6ce3f60f431 /lib/xdsconfig | |
parent | ca3e1762832b27dc25cf90125b376c56e24e2db2 (diff) |
Initial main commit.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib/xdsconfig')
-rw-r--r-- | lib/xdsconfig/builderconfig.go | 50 | ||||
-rw-r--r-- | lib/xdsconfig/config.go | 231 | ||||
-rw-r--r-- | lib/xdsconfig/fileconfig.go | 133 | ||||
-rw-r--r-- | lib/xdsconfig/folderconfig.go | 79 | ||||
-rw-r--r-- | lib/xdsconfig/foldersconfig.go | 47 |
5 files changed, 540 insertions, 0 deletions
diff --git a/lib/xdsconfig/builderconfig.go b/lib/xdsconfig/builderconfig.go new file mode 100644 index 0000000..c64fe9c --- /dev/null +++ b/lib/xdsconfig/builderconfig.go @@ -0,0 +1,50 @@ +package xdsconfig + +import ( + "errors" + "net" +) + +// BuilderConfig represents the builder container configuration +type BuilderConfig struct { + IP string `json:"ip"` + Port string `json:"port"` + SyncThingID string `json:"syncThingID"` +} + +// NewBuilderConfig creates a new BuilderConfig instance +func NewBuilderConfig(stID string) (BuilderConfig, error) { + // Do we really need it ? may be not accessible from client side + ip, err := getLocalIP() + if err != nil { + return BuilderConfig{}, err + } + + b := BuilderConfig{ + IP: ip, // TODO currently not used + Port: "", // TODO currently not used + SyncThingID: stID, + } + return b, nil +} + +// Copy makes a real copy of BuilderConfig +func (c *BuilderConfig) Copy(n BuilderConfig) { + // TODO +} + +func getLocalIP() (string, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "", err + } + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String(), nil + } + } + } + return "", errors.New("Cannot determined local IP") +} diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go new file mode 100644 index 0000000..df98439 --- /dev/null +++ b/lib/xdsconfig/config.go @@ -0,0 +1,231 @@ +package xdsconfig + +import ( + "fmt" + "strings" + + "os" + + "time" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "github.com/iotbzh/xds-server/lib/syncthing" +) + +// Config parameters (json format) of /config command +type Config struct { + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + VersionGitTag string `json:"gitTag"` + Builder BuilderConfig `json:"builder"` + Folders FoldersConfig `json:"folders"` + + // Private / un-exported fields + progName string + fileConf FileConfig + WebAppDir string `json:"-"` + HTTPPort string `json:"-"` + ShareRootDir string `json:"-"` + Log *logrus.Logger `json:"-"` + SThg *st.SyncThing `json:"-"` +} + +// Config default values +const ( + DefaultAPIVersion = "1" + DefaultPort = "8000" + DefaultShareDir = "/mnt/share" + DefaultLogLevel = "error" +) + +// Init loads the configuration on start-up +func Init(ctx *cli.Context) (Config, error) { + var err error + + // Set logger level and formatter + log := ctx.App.Metadata["logger"].(*logrus.Logger) + + logLevel := ctx.GlobalString("log") + if logLevel == "" { + logLevel = DefaultLogLevel + } + if log.Level, err = logrus.ParseLevel(logLevel); err != nil { + fmt.Printf("Invalid log level : \"%v\"\n", logLevel) + os.Exit(1) + } + log.Formatter = &logrus.TextFormatter{} + + // Define default configuration + c := Config{ + Version: ctx.App.Metadata["version"].(string), + APIVersion: DefaultAPIVersion, + VersionGitTag: ctx.App.Metadata["git-tag"].(string), + Builder: BuilderConfig{}, + Folders: FoldersConfig{}, + + progName: ctx.App.Name, + WebAppDir: "webapp/dist", + HTTPPort: DefaultPort, + ShareRootDir: DefaultShareDir, + Log: log, + SThg: nil, + } + + // config file settings overwrite default config + err = updateConfigFromFile(&c, ctx.GlobalString("config")) + if err != nil { + return Config{}, err + } + + // Update location of shared dir if needed + if !dirExists(c.ShareRootDir) { + if err := os.MkdirAll(c.ShareRootDir, 0770); err != nil { + c.Log.Fatalf("No valid shared directory found (err=%v)", err) + } + } + c.Log.Infoln("Share root directory: ", c.ShareRootDir) + + // FIXME - add a builder interface and support other builder type (eg. native) + builderType := "syncthing" + + switch builderType { + case "syncthing": + // Syncthing settings only configurable from config.json file + stGuiAddr := c.fileConf.SThgConf.GuiAddress + stGuiApikey := c.fileConf.SThgConf.GuiAPIKey + if stGuiAddr == "" { + stGuiAddr = "http://localhost:8384" + } + if stGuiAddr[0:7] != "http://" { + stGuiAddr = "http://" + stGuiAddr + } + + // Retry if connection fail + retry := 5 + for retry > 0 { + c.SThg = st.NewSyncThing(stGuiAddr, stGuiApikey, c.Log) + if c.SThg != nil { + break + } + c.Log.Warningf("Establishing connection to Syncthing (retry %d/5)", retry) + time.Sleep(time.Second) + retry-- + } + if c.SThg == nil { + c.Log.Fatalf("ERROR: cannot connect to Syncthing (url: %s)", stGuiAddr) + } + + // Retrieve Syncthing config + id, err := c.SThg.IDGet() + if err != nil { + return Config{}, err + } + + if c.Builder, err = NewBuilderConfig(id); err != nil { + c.Log.Fatalln(err) + } + + // Retrieve initial Syncthing config + stCfg, err := c.SThg.ConfigGet() + if err != nil { + return Config{}, err + } + for _, stFld := range stCfg.Folders { + relativePath := strings.TrimPrefix(stFld.RawPath, c.ShareRootDir) + if relativePath == "" { + relativePath = stFld.RawPath + } + newFld := NewFolderConfig(stFld.ID, stFld.Label, c.ShareRootDir, strings.Trim(relativePath, "/")) + c.Folders = c.Folders.Update(FoldersConfig{newFld}) + } + + default: + log.Fatalln("Unsupported builder type") + } + + return c, nil +} + +// GetFolderFromID retrieves the Folder config from id +func (c *Config) GetFolderFromID(id string) *FolderConfig { + if idx := c.Folders.GetIdx(id); idx != -1 { + return &c.Folders[idx] + } + return nil +} + +// UpdateAll updates all the current configuration +func (c *Config) UpdateAll(newCfg Config) error { + return fmt.Errorf("Not Supported") + /* + if err := VerifyConfig(newCfg); err != nil { + return err + } + + // TODO: c.Builder = c.Builder.Update(newCfg.Builder) + c.Folders = c.Folders.Update(newCfg.Folders) + + // SEB A SUP model.NotifyListeners(c, NotifyFoldersChange, FolderConfig{}) + // FIXME To be tested & improved error handling + for _, f := range c.Folders { + if err := c.SThg.FolderChange(st.FolderChangeArg{ + ID: f.ID, + Label: f.Label, + RelativePath: f.RelativePath, + SyncThingID: f.SyncThingID, + ShareRootDir: c.ShareRootDir, + }); err != nil { + return err + } + } + + return nil + */ +} + +// UpdateFolder updates a specific folder into the current configuration +func (c *Config) UpdateFolder(newFolder FolderConfig) (FolderConfig, error) { + if err := FolderVerify(newFolder); err != nil { + return FolderConfig{}, err + } + + c.Folders = c.Folders.Update(FoldersConfig{newFolder}) + + // SEB A SUP model.NotifyListeners(c, NotifyFolderAdd, newFolder) + err := c.SThg.FolderChange(st.FolderChangeArg{ + ID: newFolder.ID, + Label: newFolder.Label, + RelativePath: newFolder.RelativePath, + SyncThingID: newFolder.SyncThingID, + ShareRootDir: c.ShareRootDir, + }) + + newFolder.BuilderSThgID = c.Builder.SyncThingID // FIXME - should be removed after local ST config rework + newFolder.Status = FolderStatusEnable + + return newFolder, err +} + +// DeleteFolder deletes a specific folder +func (c *Config) DeleteFolder(id string) (FolderConfig, error) { + var fld FolderConfig + var err error + + //SEB A SUP model.NotifyListeners(c, NotifyFolderDelete, fld) + if err = c.SThg.FolderDelete(id); err != nil { + return fld, err + } + + c.Folders, fld, err = c.Folders.Delete(id) + + return fld, err +} + +func dirExists(path string) bool { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return false + } + return true +} diff --git a/lib/xdsconfig/fileconfig.go b/lib/xdsconfig/fileconfig.go new file mode 100644 index 0000000..262d023 --- /dev/null +++ b/lib/xdsconfig/fileconfig.go @@ -0,0 +1,133 @@ +package xdsconfig + +import ( + "encoding/json" + "os" + "os/user" + "path" + "path/filepath" + "regexp" + "strings" +) + +type SyncThingConf struct { + Home string `json:"home"` + GuiAddress string `json:"gui-address"` + GuiAPIKey string `json:"gui-apikey"` +} + +type FileConfig struct { + WebAppDir string `json:"webAppDir"` + ShareRootDir string `json:"shareRootDir"` + HTTPPort string `json:"httpPort"` + SThgConf SyncThingConf `json:"syncthing"` +} + +// getConfigFromFile reads configuration from a config file. +// Order to determine which config file is used: +// 1/ from command line option: "--config myConfig.json" +// 2/ $HOME/.xds/config.json file +// 3/ <xds-server executable dir>/config.json file + +func updateConfigFromFile(c *Config, confFile string) error { + + searchIn := make([]string, 0, 3) + if confFile != "" { + searchIn = append(searchIn, confFile) + } + if usr, err := user.Current(); err == nil { + searchIn = append(searchIn, path.Join(usr.HomeDir, ".xds", "config.json")) + } + cwd, err := os.Getwd() + if err == nil { + searchIn = append(searchIn, path.Join(cwd, "config.json")) + } + exePath, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err == nil { + searchIn = append(searchIn, path.Join(exePath, "config.json")) + } + + var cFile *string + for _, p := range searchIn { + if _, err := os.Stat(p); err == nil { + cFile = &p + break + } + } + if cFile == nil { + // No config file found + return nil + } + + // TODO move on viper package to support comments in JSON and also + // bind with flags (command line options) + // see https://github.com/spf13/viper#working-with-flags + + fd, _ := os.Open(*cFile) + defer fd.Close() + fCfg := FileConfig{} + if err := json.NewDecoder(fd).Decode(&fCfg); err != nil { + return err + } + c.fileConf = fCfg + + // Support environment variables (IOW ${MY_ENV_VAR} syntax) in config.json + // TODO: better to use reflect package to iterate on fields and be more generic + fCfg.WebAppDir = path.Clean(resolveEnvVar(fCfg.WebAppDir)) + fCfg.ShareRootDir = path.Clean(resolveEnvVar(fCfg.ShareRootDir)) + fCfg.SThgConf.Home = path.Clean(resolveEnvVar(fCfg.SThgConf.Home)) + + // Config file settings overwrite default config + + if fCfg.WebAppDir != "" { + c.WebAppDir = strings.Trim(fCfg.WebAppDir, " ") + } + // Is it a full path ? + if !strings.HasPrefix(c.WebAppDir, "/") && exePath != "" { + // Check first from current directory + for _, rootD := range []string{cwd, exePath} { + ff := path.Join(rootD, c.WebAppDir, "index.html") + if exists(ff) { + c.WebAppDir = path.Join(rootD, c.WebAppDir) + break + } + } + } + + if fCfg.ShareRootDir != "" { + c.ShareRootDir = fCfg.ShareRootDir + } + + if fCfg.HTTPPort != "" { + c.HTTPPort = fCfg.HTTPPort + } + + return nil +} + +// resolveEnvVar Resolved environment variable regarding the syntax ${MYVAR} +func resolveEnvVar(s string) string { + re := regexp.MustCompile("\\${(.*)}") + vars := re.FindAllStringSubmatch(s, -1) + res := s + for _, v := range vars { + val := os.Getenv(v[1]) + if val != "" { + rer := regexp.MustCompile("\\${" + v[1] + "}") + res = rer.ReplaceAllString(res, val) + } + } + return res +} + +// exists returns whether the given file or directory exists or not +func exists(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return true +} diff --git a/lib/xdsconfig/folderconfig.go b/lib/xdsconfig/folderconfig.go new file mode 100644 index 0000000..e8bff4f --- /dev/null +++ b/lib/xdsconfig/folderconfig.go @@ -0,0 +1,79 @@ +package xdsconfig + +import ( + "fmt" + "log" + "path/filepath" +) + +// FolderType constances +const ( + FolderTypeDocker = 0 + FolderTypeWindowsSubsystem = 1 + FolderTypeCloudSync = 2 + + FolderStatusErrorConfig = "ErrorConfig" + FolderStatusDisable = "Disable" + FolderStatusEnable = "Enable" +) + +// FolderType is the type of sharing folder +type FolderType int + +// FolderConfig is the config for one folder +type FolderConfig struct { + ID string `json:"id" binding:"required"` + Label string `json:"label"` + RelativePath string `json:"path"` + Type FolderType `json:"type"` + SyncThingID string `json:"syncThingID"` + BuilderSThgID string `json:"builderSThgID"` + Status string `json:"status"` + + // Private fields + rootPath string +} + +// NewFolderConfig creates a new folder object +func NewFolderConfig(id, label, rootDir, path string) FolderConfig { + return FolderConfig{ + ID: id, + Label: label, + RelativePath: path, + Type: FolderTypeCloudSync, + SyncThingID: "", + Status: FolderStatusDisable, + rootPath: rootDir, + } +} + +// GetFullPath returns the full path +func (c *FolderConfig) GetFullPath(dir string) string { + if &dir == nil { + dir = "" + } + if filepath.IsAbs(dir) { + return filepath.Join(c.rootPath, dir) + } + return filepath.Join(c.rootPath, c.RelativePath, dir) +} + +// FolderVerify is called to verify that a configuration is valid +func FolderVerify(fCfg FolderConfig) error { + var err error + + if fCfg.Type != FolderTypeCloudSync { + err = fmt.Errorf("Unsupported folder type") + } + + if fCfg.SyncThingID == "" { + err = fmt.Errorf("device id not set (SyncThingID field)") + } + + if err != nil { + fCfg.Status = FolderStatusErrorConfig + log.Printf("ERROR FolderVerify: %v\n", err) + } + + return err +} diff --git a/lib/xdsconfig/foldersconfig.go b/lib/xdsconfig/foldersconfig.go new file mode 100644 index 0000000..4ad16df --- /dev/null +++ b/lib/xdsconfig/foldersconfig.go @@ -0,0 +1,47 @@ +package xdsconfig + +import ( + "fmt" +) + +// FoldersConfig contains all the folder configurations +type FoldersConfig []FolderConfig + +// GetIdx returns the index of the folder matching id in FoldersConfig array +func (c FoldersConfig) GetIdx(id string) int { + for i := range c { + if id == c[i].ID { + return i + } + } + return -1 +} + +// Update is used to fully update or add a new FolderConfig +func (c FoldersConfig) Update(newCfg FoldersConfig) FoldersConfig { + for i := range newCfg { + found := false + for j := range c { + if newCfg[i].ID == c[j].ID { + c[j] = newCfg[i] + found = true + break + } + } + if !found { + c = append(c, newCfg[i]) + } + } + return c +} + +// Delete is used to delete a folder matching id in FoldersConfig array +func (c FoldersConfig) Delete(id string) (FoldersConfig, FolderConfig, error) { + if idx := c.GetIdx(id); idx != -1 { + f := c[idx] + c = append(c[:idx], c[idx+1:]...) + return c, f, nil + } + + return c, FolderConfig{}, fmt.Errorf("invalid id") +} |