Storage Module Basis (#13)
* Added storage module, base structs, getDevices() func, taskGetStorageDevices (ticks every 30) * GetDevices now returns a complete description w/ disk partitions * Added InUse flag to Device, fixed logic bugs * Added disk space usage for partitions in use by the system * Cleanup * Added UsageStat and UsageSplit filling for in use devices * Changed value of lsblk block usage output to bytes * Cleanup, small fixes * Added ExecAndGetLines util func * Added explicit detection of disk or part block type, ignoring non-suppoorted devices * Added command line tool dependencies for the project in README file * Added comment to func ExecAndGetLines * Removed GetMySQLDbConnectionDetails() * Added tests to utils module * Fix invalid argument type for Exec family of tests * Removed too much verbosity from utils.GetPath when no env file is available, added new argument (path) to Exec family of funcs * Added new path argument to utils.Exec... test * Fixed TestExec * Added spaces trim to Exec result * Trimming spaces and newlines from result of Exec * Better visibility for test result log of TestExec * Added ExampleExecAndStream test * Added Example tests with failure status for ExecAndStream * Added branch to test for valid key in GetPath * Added missing t.Fail() statements in utils_test.go * Added storage_test.go file, smoke test * TestGetDevicespull/18/head 0.0.1
							parent
							
								
									92ac9c2b03
								
							
						
					
					
						commit
						9339e6cf09
					
				
								
									
									
										
											27
										
									
									README.md
									
									
									
									
								
								
							
							
										
											27
										
									
									README.md
									
									
									
									
								|  | @ -42,15 +42,16 @@ | |||
| <!-- TABLE OF CONTENTS --> | ||||
| ## Table of Contents | ||||
| 
 | ||||
| * [About the Project](#about-the-project) | ||||
|   * [Built With](#built-with) | ||||
| * [Getting Started](#getting-started) | ||||
|   * [Prerequisites](#prerequisites) | ||||
|   * [Installation](#installation) | ||||
| * [Usage](#usage) | ||||
| * [Roadmap](#roadmap) | ||||
| * [Contributing](#contributing) | ||||
| * [License](#license) | ||||
| - [Table of Contents](#table-of-contents) | ||||
| - [About The Project](#about-the-project) | ||||
|   - [Built With](#built-with) | ||||
| - [Getting Started](#getting-started) | ||||
|   - [Prerequisites](#prerequisites) | ||||
|   - [Installation](#installation) | ||||
| - [Usage](#usage) | ||||
| - [Roadmap](#roadmap) | ||||
| - [Contributing](#contributing) | ||||
| - [License](#license) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -88,6 +89,14 @@ sudo apt-get install docker docker-compose | |||
| 
 | ||||
| Check the following links for more info on [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/). | ||||
| 
 | ||||
| Aditionally, edgeboxctl needs the following bash commands available wherever it runs: | ||||
| * `sh` | ||||
| * `rm` | ||||
| * `systemctl` | ||||
| * `lsblk` | ||||
| * `yq` | ||||
| * `tinc-boot` _(not mandatory)_ | ||||
| 
 | ||||
| ### Installation | ||||
| 
 | ||||
| 1. Clone the repo | ||||
|  |  | |||
								
									
									
										
											2
										
									
									go.mod
									
									
									
									
								
								
							
							
										
											2
										
									
									go.mod
									
									
									
									
								|  | @ -4,6 +4,8 @@ go 1.15 | |||
| 
 | ||||
| require ( | ||||
| 	github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect | ||||
| 	github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.0 // indirect | ||||
| 	github.com/go-ole/go-ole v1.2.5 // indirect | ||||
| 	github.com/go-sql-driver/mysql v1.5.0 | ||||
| 	github.com/joho/godotenv v1.3.0 | ||||
|  |  | |||
								
									
									
										
											4
										
									
									go.sum
									
									
									
									
								
								
							
							
										
											4
										
									
									go.sum
									
									
									
									
								|  | @ -1,8 +1,12 @@ | |||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY= | ||||
| github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= | ||||
| github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2 h1:5EPE4Uk7ucthLTJAZqZxu6LZluox5/AqXUxJDpzgJjg= | ||||
| github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2/go.mod h1:NDZpkezJ8QqyIW/510MywB5T2KdC8v/0oTlEoPcMsRM= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= | ||||
| github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= | ||||
| github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= | ||||
| github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= | ||||
| github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= | ||||
|  |  | |||
|  | @ -239,14 +239,14 @@ func GetEdgeAppStatus(ID string) EdgeAppStatus { | |||
| func GetEdgeAppServices(ID string) []EdgeAppService { | ||||
| 
 | ||||
| 	cmdArgs := []string{"-r", ".services | keys[]", utils.GetPath("edgeAppsPath") + ID + configFilename} | ||||
| 	servicesString := utils.Exec("yq", cmdArgs) | ||||
| 	servicesString := utils.Exec(utils.GetPath("wsPath"), "yq", cmdArgs) | ||||
| 	serviceSlices := strings.Split(servicesString, "\n") | ||||
| 	serviceSlices = utils.DeleteEmptySlices(serviceSlices) | ||||
| 	var edgeAppServices []EdgeAppService | ||||
| 
 | ||||
| 	for _, serviceID := range serviceSlices { | ||||
| 		cmdArgs = []string{"-f", utils.GetPath("wsPath") + "/docker-compose.yml", "exec", "-T", serviceID, "echo", "'Service Check'"} | ||||
| 		cmdResult := utils.Exec("docker-compose", cmdArgs) | ||||
| 		cmdResult := utils.Exec(utils.GetPath("wsPath"), "docker-compose", cmdArgs) | ||||
| 		isRunning := false | ||||
| 		if cmdResult != "" { | ||||
| 			isRunning = true | ||||
|  | @ -268,7 +268,7 @@ func RunEdgeApp(ID string) EdgeAppStatus { | |||
| 	for _, service := range services { | ||||
| 
 | ||||
| 		cmdArgs = []string{"-f", utils.GetPath("wsPath") + "/docker-compose.yml", "start", service.ID} | ||||
| 		utils.Exec("docker-compose", cmdArgs) | ||||
| 		utils.Exec(utils.GetPath("wsPath"), "docker-compose", cmdArgs) | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
|  | @ -289,7 +289,7 @@ func StopEdgeApp(ID string) EdgeAppStatus { | |||
| 	for _, service := range services { | ||||
| 
 | ||||
| 		cmdArgs = []string{"-f", utils.GetPath("wsPath") + "/docker-compose.yml", "stop", service.ID} | ||||
| 		utils.Exec("docker-compose", cmdArgs) | ||||
| 		utils.Exec(utils.GetPath("wsPath"), "docker-compose", cmdArgs) | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
|  | @ -326,7 +326,7 @@ func DisableOnline(ID string) MaybeEdgeApp { | |||
| 		log.Println("myedge.app environment file for " + ID + " not found. No need to delete.") | ||||
| 	} else { | ||||
| 		cmdArgs := []string{envFilePath} | ||||
| 		utils.Exec("rm", cmdArgs) | ||||
| 		utils.Exec(utils.GetPath("wsPath"), "rm", cmdArgs) | ||||
| 	} | ||||
| 
 | ||||
| 	buildFrameworkContainers() | ||||
|  | @ -338,7 +338,7 @@ func DisableOnline(ID string) MaybeEdgeApp { | |||
| func buildFrameworkContainers() { | ||||
| 
 | ||||
| 	cmdArgs := []string{utils.GetPath("wsPath") + "ws", "--build"} | ||||
| 	utils.ExecAndStream("sh", cmdArgs) | ||||
| 	utils.ExecAndStream(utils.GetPath("wsPath"), "sh", cmdArgs) | ||||
| 
 | ||||
| 	time.Sleep(defaultContainerOperationSleepTime) | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,272 @@ | |||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/edgebox-iot/edgeboxctl/internal/utils" | ||||
| 	"github.com/shirou/gopsutil/disk" | ||||
| ) | ||||
| 
 | ||||
| // Device : Struct representing a storage device in the system
 | ||||
| type Device struct { | ||||
| 	ID         string       `json:"id"` | ||||
| 	Name       string       `json:"name"` | ||||
| 	Size       string       `json:"size"` | ||||
| 	InUse      bool         `json:"in_use"` | ||||
| 	MainDevice bool         `json:"main_device"` | ||||
| 	MAJ        string       `json:"maj"` | ||||
| 	MIN        string       `json:"min"` | ||||
| 	RM         string       `json:"rm"` | ||||
| 	RO         string       `json:"ro"` | ||||
| 	Partitions []Partition  `json:"partitions"` | ||||
| 	Status     DeviceStatus `json:"status"` | ||||
| 	UsageStat  UsageStat    `json:"usage_stat"` | ||||
| } | ||||
| 
 | ||||
| // DeviceStatus : Struct representing possible storage device statuses (code + description)
 | ||||
| type DeviceStatus struct { | ||||
| 	ID          int    `json:"id"` | ||||
| 	Description string `json:"description"` | ||||
| } | ||||
| 
 | ||||
| // MaybeDevice : Boolean flag for validation of device existance
 | ||||
| type MaybeDevice struct { | ||||
| 	Device Device `json:"device"` | ||||
| 	Valid  bool   `json:"valid"` | ||||
| } | ||||
| 
 | ||||
| type UsageStat struct { | ||||
| 	Total      uint64     `json:"total"` | ||||
| 	Used       uint64     `json:"used"` | ||||
| 	Free       uint64     `json:"free"` | ||||
| 	Percent    string     `json:"percent"` | ||||
| 	UsageSplit UsageSplit `json:"usage_split"` | ||||
| } | ||||
| 
 | ||||
| type UsageSplit struct { | ||||
| 	OS       uint64 `json:"os"` | ||||
| 	EdgeApps uint64 `json:"edgeapps"` | ||||
| 	Buckets  uint64 `json:"buckets"` | ||||
| 	Others   uint64 `json:"others"` | ||||
| } | ||||
| 
 | ||||
| // Partition : Struct representing a partition / filesystem (Empty Mountpoint means it is not mounted)
 | ||||
| type Partition struct { | ||||
| 	ID         string    `json:"id"` | ||||
| 	Size       string    `json:"size"` | ||||
| 	MAJ        string    `json:"maj"` | ||||
| 	MIN        string    `json:"min"` | ||||
| 	RM         string    `json:"rm"` | ||||
| 	RO         string    `json:"ro"` | ||||
| 	Filesystem string    `json:"filesystem"` | ||||
| 	Mountpoint string    `json:"mountpoint"` | ||||
| 	UsageStat  UsageStat `json:"usage_stat"` | ||||
| } | ||||
| 
 | ||||
| const mainDiskID = "mmcblk0" | ||||
| 
 | ||||
| // GetDevices : Returns a list of all available sotrage devices in structs filled with information
 | ||||
| func GetDevices() []Device { | ||||
| 
 | ||||
| 	var devices []Device | ||||
| 
 | ||||
| 	cmdArgs := []string{"--raw", "--bytes", "--noheadings"} | ||||
| 	scanner := utils.ExecAndGetLines("/", "lsblk", cmdArgs) | ||||
| 
 | ||||
| 	var currentDevice Device | ||||
| 	var currentPartitions []Partition | ||||
| 
 | ||||
| 	firstDevice := true | ||||
| 	currentDeviceInUseFlag := false | ||||
| 
 | ||||
| 	for scanner.Scan() { | ||||
| 		// 1 Device is represented here. Extract words in order for filling a Device struct
 | ||||
| 		// Example deviceRawInfo: "mmcblk0 179:0 0 29.7G 0 disk"
 | ||||
| 		deviceRawInfo := strings.Fields(scanner.Text()) | ||||
| 		majMin := strings.SplitN(deviceRawInfo[1], ":", 2) | ||||
| 
 | ||||
| 		isDevice := false | ||||
| 		isPartition := false | ||||
| 		if deviceRawInfo[5] == "part" { | ||||
| 			isDevice = false | ||||
| 			isPartition = true | ||||
| 
 | ||||
| 		} else if deviceRawInfo[5] == "disk" { | ||||
| 			isDevice = true | ||||
| 			isPartition = false | ||||
| 		} | ||||
| 
 | ||||
| 		if isDevice { | ||||
| 			// Clean up on the latest device being prepared. Append all partitions found and delete the currentPartitions list afterwards.
 | ||||
| 			// The first device found should not run the cleanup lines below
 | ||||
| 
 | ||||
| 			if !firstDevice { | ||||
| 				currentDevice.Partitions = currentPartitions | ||||
| 
 | ||||
| 				if !currentDeviceInUseFlag { | ||||
| 					currentDevice.Status.ID = 0 | ||||
| 					currentDevice.Status.Description = "not configured" | ||||
| 				} | ||||
| 
 | ||||
| 				currentDevice.InUse = currentDeviceInUseFlag | ||||
| 				currentDeviceInUseFlag = false | ||||
| 				currentPartitions = []Partition{} | ||||
| 				devices = append(devices, currentDevice) | ||||
| 			} else { | ||||
| 				firstDevice = false | ||||
| 			} | ||||
| 
 | ||||
| 			mainDevice := false | ||||
| 
 | ||||
| 			device := Device{ | ||||
| 				ID:         deviceRawInfo[0], | ||||
| 				Name:       deviceRawInfo[0], | ||||
| 				Size:       deviceRawInfo[3], | ||||
| 				MainDevice: mainDevice, | ||||
| 				MAJ:        majMin[0], | ||||
| 				MIN:        majMin[1], | ||||
| 				RM:         deviceRawInfo[2], | ||||
| 				RO:         deviceRawInfo[4], | ||||
| 				Status:     DeviceStatus{ID: 1, Description: "healthy"}, | ||||
| 			} | ||||
| 
 | ||||
| 			if device.ID == mainDiskID { | ||||
| 				device.MainDevice = true | ||||
| 			} | ||||
| 
 | ||||
| 			currentDevice = device | ||||
| 
 | ||||
| 		} else if isPartition { | ||||
| 
 | ||||
| 			mountpoint := "" | ||||
| 			if len(deviceRawInfo) >= 7 { | ||||
| 				mountpoint = deviceRawInfo[6] | ||||
| 				currentDeviceInUseFlag = true | ||||
| 			} | ||||
| 
 | ||||
| 			// It is a partition, part of the last device read.
 | ||||
| 			partition := Partition{ | ||||
| 				ID:         deviceRawInfo[0], | ||||
| 				Size:       deviceRawInfo[3], | ||||
| 				MAJ:        majMin[0], | ||||
| 				MIN:        majMin[1], | ||||
| 				RM:         deviceRawInfo[2], | ||||
| 				RO:         deviceRawInfo[4], | ||||
| 				Filesystem: "", | ||||
| 				Mountpoint: mountpoint, | ||||
| 			} | ||||
| 
 | ||||
| 			currentPartitions = append(currentPartitions, partition) | ||||
| 
 | ||||
| 		} else { | ||||
| 			fmt.Println("Found device not compatible with Edgebox, ignoring.") | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	currentDevice.Partitions = currentPartitions | ||||
| 	if !currentDeviceInUseFlag { | ||||
| 		currentDevice.Status.ID = 0 | ||||
| 		currentDevice.Status.Description = "Not configured" | ||||
| 	} | ||||
| 	currentDevice.InUse = currentDeviceInUseFlag | ||||
| 	devices = append([]Device{currentDevice}, devices...) // Prepending the first device...
 | ||||
| 
 | ||||
| 	devices = getDevicesSpaceUsage(devices) | ||||
| 
 | ||||
| 	return devices | ||||
| } | ||||
| 
 | ||||
| func getDevicesSpaceUsage(devices []Device) []Device { | ||||
| 
 | ||||
| 	for deviceIndex, device := range devices { | ||||
| 
 | ||||
| 		if device.InUse { | ||||
| 
 | ||||
| 			deviceUsageStat := UsageStat{} | ||||
| 
 | ||||
| 			for partitionIndex, partition := range device.Partitions { | ||||
| 
 | ||||
| 				if partition.Mountpoint != "" { | ||||
| 
 | ||||
| 					s, _ := disk.Usage(partition.Mountpoint) | ||||
| 
 | ||||
| 					if s.Total == 0 { | ||||
| 						continue | ||||
| 					} | ||||
| 
 | ||||
| 					partitionUsagePercent := fmt.Sprintf("%2.f%%", s.UsedPercent) | ||||
| 					osUsageSplit := (uint64)(0) | ||||
| 					edgeappsUsageSplit := (uint64)(0) | ||||
| 					bucketsUsageSplit := (uint64)(0) | ||||
| 					othersUsageSplit := (uint64)(0) | ||||
| 
 | ||||
| 					edgeappsDirSize, _ := getDirSize(utils.GetPath("edgeAppsPath")) | ||||
| 					// TODO for later: Figure out to get correct paths for each partition...
 | ||||
| 					wsAppDataDirSize, _ := getDirSize("/home/system/components/ws/appdata") | ||||
| 
 | ||||
| 					if partition.Mountpoint == "/" { | ||||
| 						edgeappsUsageSplit = edgeappsDirSize + wsAppDataDirSize | ||||
| 						deviceUsageStat.UsageSplit.EdgeApps += edgeappsUsageSplit | ||||
| 					} | ||||
| 
 | ||||
| 					if device.MainDevice { | ||||
| 						osUsageSplit = (s.Used - othersUsageSplit - bucketsUsageSplit - edgeappsUsageSplit) | ||||
| 					} else { | ||||
| 						othersUsageSplit = (s.Used - bucketsUsageSplit - edgeappsUsageSplit) | ||||
| 					} | ||||
| 
 | ||||
| 					partitionUsageSplit := UsageSplit{ | ||||
| 						OS:       osUsageSplit, | ||||
| 						EdgeApps: edgeappsUsageSplit, | ||||
| 						Buckets:  bucketsUsageSplit, | ||||
| 						Others:   othersUsageSplit, | ||||
| 					} | ||||
| 
 | ||||
| 					deviceUsageStat.Total = deviceUsageStat.Total + s.Total | ||||
| 					deviceUsageStat.Used = deviceUsageStat.Used + s.Used | ||||
| 					deviceUsageStat.Free = deviceUsageStat.Free + s.Free | ||||
| 					deviceUsageStat.UsageSplit.OS = deviceUsageStat.UsageSplit.OS + osUsageSplit | ||||
| 					deviceUsageStat.UsageSplit.EdgeApps = deviceUsageStat.UsageSplit.EdgeApps + edgeappsUsageSplit | ||||
| 					deviceUsageStat.UsageSplit.Buckets = deviceUsageStat.UsageSplit.Buckets + bucketsUsageSplit | ||||
| 					deviceUsageStat.UsageSplit.Others = deviceUsageStat.UsageSplit.Others + othersUsageSplit | ||||
| 
 | ||||
| 					devices[deviceIndex].Partitions[partitionIndex].UsageStat = UsageStat{ | ||||
| 						Total:      s.Total, | ||||
| 						Used:       s.Used, | ||||
| 						Free:       s.Free, | ||||
| 						Percent:    partitionUsagePercent, | ||||
| 						UsageSplit: partitionUsageSplit, | ||||
| 					} | ||||
| 
 | ||||
| 				} | ||||
| 
 | ||||
| 			} | ||||
| 
 | ||||
| 			devices[deviceIndex].UsageStat = deviceUsageStat | ||||
| 			totalDevicePercentUsage := fmt.Sprintf("%2.f%%", (float32(devices[deviceIndex].UsageStat.Used)/float32(devices[deviceIndex].UsageStat.Total))*100) | ||||
| 			devices[deviceIndex].UsageStat.Percent = totalDevicePercentUsage | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	return devices | ||||
| } | ||||
| 
 | ||||
| func getDirSize(path string) (uint64, error) { | ||||
| 	var size uint64 | ||||
| 	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if !info.IsDir() { | ||||
| 			size += (uint64)(info.Size()) | ||||
| 		} | ||||
| 		return err | ||||
| 	}) | ||||
| 	return size, err | ||||
| } | ||||
|  | @ -0,0 +1,31 @@ | |||
| // +build unit
 | ||||
| 
 | ||||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestGetDevices(t *testing.T) { | ||||
| 	result := GetDevices() | ||||
| 
 | ||||
| 	if len(result) == 0 { | ||||
| 		t.Log("Expecting at least 1 block device, 0 elements found in slice") | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 
 | ||||
| 	foundDevice := false | ||||
| 
 | ||||
| 	t.Log("Looking for a mmcblk0 or sda device") | ||||
| 	for _, device := range result { | ||||
| 		if device.ID == "mmcblk0" || device.ID == "sda" { | ||||
| 			t.Log("Found target device", device.ID) | ||||
| 			foundDevice = true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !foundDevice { | ||||
| 		t.Log("Expected to find device mmcblk0 but did not. Devices:", result) | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
|  | @ -10,6 +10,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/edgebox-iot/edgeboxctl/internal/diagnostics" | ||||
| 	"github.com/edgebox-iot/edgeboxctl/internal/edgeapps" | ||||
| 	"github.com/edgebox-iot/edgeboxctl/internal/storage" | ||||
| 	"github.com/edgebox-iot/edgeboxctl/internal/system" | ||||
| 	"github.com/edgebox-iot/edgeboxctl/internal/utils" | ||||
| 	_ "github.com/go-sql-driver/mysql" // Mysql Driver
 | ||||
|  | @ -253,10 +254,16 @@ func ExecuteSchedules(tick int) { | |||
| 		uptime := taskGetSystemUptime() | ||||
| 		log.Println("Uptime is " + uptime + " seconds (" + system.GetUptimeFormatted() + ")") | ||||
| 
 | ||||
| 		log.Println(taskGetStorageDevices()) | ||||
| 		log.Println(taskGetEdgeApps()) | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	if tick%5 == 0 { | ||||
| 		// Executing every 5 ticks
 | ||||
| 		log.Println(taskGetStorageDevices()) | ||||
| 	} | ||||
| 
 | ||||
| 	if tick%30 == 0 { | ||||
| 		// Executing every 30 ticks
 | ||||
| 		log.Println(taskGetEdgeApps()) | ||||
|  | @ -277,13 +284,13 @@ func taskSetupTunnel(args taskSetupTunnelArgs) string { | |||
| 	fmt.Println("Executing taskSetupTunnel") | ||||
| 
 | ||||
| 	cmdargs := []string{"gen", "--name", args.NodeName, "--token", args.BootnodeToken, args.BootnodeAddress + ":8655", "--prefix", args.AssignedAddress} | ||||
| 	utils.Exec("tinc-boot", cmdargs) | ||||
| 	utils.Exec(utils.GetPath("wsPath"), "tinc-boot", cmdargs) | ||||
| 
 | ||||
| 	cmdargs = []string{"start", "tinc@dnet"} | ||||
| 	utils.Exec("systemctl", cmdargs) | ||||
| 	utils.Exec(utils.GetPath("wsPath"), "systemctl", cmdargs) | ||||
| 
 | ||||
| 	cmdargs = []string{"enable", "tinc@dnet"} | ||||
| 	utils.Exec("systemctl", cmdargs) | ||||
| 	utils.Exec(utils.GetPath("wsPath"), "systemctl", cmdargs) | ||||
| 
 | ||||
| 	output := "OK" // Better check / logging of command execution result.
 | ||||
| 	return output | ||||
|  | @ -435,3 +442,33 @@ func taskGetSystemUptime() string { | |||
| 	return uptime | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func taskGetStorageDevices() string { | ||||
| 	fmt.Println("Executing taskGetStorageDevices") | ||||
| 
 | ||||
| 	devices := storage.GetDevices() | ||||
| 	devicesJSON, _ := json.Marshal(devices) | ||||
| 
 | ||||
| 	db, err := sql.Open("sqlite3", utils.GetSQLiteDbConnectionDetails()) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	statement, err := db.Prepare("REPLACE into option (name, value, created, updated) VALUES (?, ?, ?, ?);") // Prepare SQL Statement
 | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	formatedDatetime := utils.GetSQLiteFormattedDateTime(time.Now()) | ||||
| 
 | ||||
| 	_, err = statement.Exec("STORAGE_DEVICES_LIST", devicesJSON, formatedDatetime, formatedDatetime) // Execute SQL Statement
 | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	db.Close() | ||||
| 
 | ||||
| 	return string(devicesJSON) | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,31 +1,33 @@ | |||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/joho/godotenv" | ||||
| ) | ||||
| 
 | ||||
| // ExecAndStream : Runs a terminal command, but streams progress instead of outputting. Ideal for long lived process that need to be logged.
 | ||||
| func ExecAndStream(command string, args []string) { | ||||
| func ExecAndStream(path string, command string, args []string) { | ||||
| 
 | ||||
| 	cmd := exec.Command(command, args...) | ||||
| 
 | ||||
| 	var stdoutBuf, stderrBuf bytes.Buffer | ||||
| 	cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) | ||||
| 	cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) | ||||
| 	cmd.Dir = GetPath("wsPath") | ||||
| 	cmd.Dir = path | ||||
| 
 | ||||
| 	err := cmd.Run() | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("cmd.Run() failed with %s\n", err) | ||||
| 		fmt.Printf("cmd.Run() failed with %s\n", err) | ||||
| 	} | ||||
| 
 | ||||
| 	outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes()) | ||||
|  | @ -34,13 +36,13 @@ func ExecAndStream(command string, args []string) { | |||
| } | ||||
| 
 | ||||
| // Exec : Runs a terminal Command, catches and logs errors, returns the result.
 | ||||
| func Exec(command string, args []string) string { | ||||
| func Exec(path string, command string, args []string) string { | ||||
| 	cmd := exec.Command(command, args...) | ||||
| 	var out bytes.Buffer | ||||
| 	var stderr bytes.Buffer | ||||
| 	cmd.Stdout = &out | ||||
| 	cmd.Stderr = &stderr | ||||
| 	cmd.Dir = GetPath("wsPath") | ||||
| 	cmd.Dir = path | ||||
| 	err := cmd.Run() | ||||
| 	if err != nil { | ||||
| 		// TODO: Deal with possibility of error in command, allow explicit error handling and return proper formatted stderr
 | ||||
|  | @ -49,10 +51,20 @@ func Exec(command string, args []string) string { | |||
| 
 | ||||
| 	// log.Println("Result: " + out.String()) // ... Silence ...
 | ||||
| 
 | ||||
| 	return out.String() | ||||
| 	return strings.Trim(out.String(), " \n") | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // Exec : Runs a terminal Command, returns the result as a *bufio.Scanner type, split in lines and ready to parse.
 | ||||
| func ExecAndGetLines(path string, command string, args []string) *bufio.Scanner { | ||||
| 	cmdOutput := Exec(path, command, args) | ||||
| 	cmdOutputReader := strings.NewReader(cmdOutput) | ||||
| 	scanner := bufio.NewScanner(cmdOutputReader) | ||||
| 	scanner.Split(bufio.ScanLines) | ||||
| 
 | ||||
| 	return scanner | ||||
| } | ||||
| 
 | ||||
| // DeleteEmptySlices : Given a string array, delete empty entries.
 | ||||
| func DeleteEmptySlices(s []string) []string { | ||||
| 	var r []string | ||||
|  | @ -64,25 +76,6 @@ func DeleteEmptySlices(s []string) []string { | |||
| 	return r | ||||
| } | ||||
| 
 | ||||
| // GetMySQLDbConnectionDetails : Returns the necessary string as connection info for SQL.db()
 | ||||
| func GetMySQLDbConnectionDetails() string { | ||||
| 
 | ||||
| 	var apiEnv map[string]string | ||||
| 	apiEnv, err := godotenv.Read(GetPath("apiEnvFileLocation")) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		log.Fatal("Error loading .env file") | ||||
| 	} | ||||
| 
 | ||||
| 	Dbhost := "127.0.0.1:" + apiEnv["HOST_MACHINE_MYSQL_PORT"] | ||||
| 	Dbname := apiEnv["MYSQL_DATABASE"] | ||||
| 	Dbuser := apiEnv["MYSQL_USER"] | ||||
| 	Dbpass := apiEnv["MYSQL_PASSWORD"] | ||||
| 
 | ||||
| 	return Dbuser + ":" + Dbpass + "@tcp(" + Dbhost + ")/" + Dbname | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // GetSQLiteDbConnectionDetails : Returns the necessary string as connection info for SQL.db()
 | ||||
| func GetSQLiteDbConnectionDetails() string { | ||||
| 
 | ||||
|  | @ -112,11 +105,10 @@ func GetPath(pathKey string) string { | |||
| 	// Read whole of .env file to map.
 | ||||
| 	var env map[string]string | ||||
| 	env, err := godotenv.Read() | ||||
| 	targetPath := "" | ||||
| 	var targetPath string | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		// log.Println("Project .env file not found withing project root. Using only hardcoded path variables.")
 | ||||
| 		// Do Nothing...
 | ||||
| 		targetPath = "" | ||||
| 	} | ||||
| 
 | ||||
| 	switch pathKey { | ||||
|  |  | |||
|  | @ -7,6 +7,96 @@ import ( | |||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func TestExec(t *testing.T) { | ||||
| 	testCommand := "echo" | ||||
| 	testArguments := []string{"Hello World"} | ||||
| 
 | ||||
| 	result := Exec("/", testCommand, testArguments) | ||||
| 
 | ||||
| 	if result != "Hello World" { | ||||
| 		t.Log("Expected 'Hello World' but got", "'"+result+"'") | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func ExampleExecAndStream() { | ||||
| 	ExecAndStream("/", "echo", []string{"Hello"}) | ||||
| 	// Output:
 | ||||
| 	// Hello
 | ||||
| 	//
 | ||||
| 	// out:
 | ||||
| 	// Hello
 | ||||
| 	//
 | ||||
| 	// err:
 | ||||
| } | ||||
| 
 | ||||
| func ExampleExecAndStreamExecutableNotFound() { | ||||
| 	ExecAndStream("/", "testcommand", []string{"Hello"}) | ||||
| 	// Output:
 | ||||
| 	// cmd.Run() failed with exec: "testcommand": executable file not found in $PATH
 | ||||
| 	//
 | ||||
| 	// out:
 | ||||
| 	//
 | ||||
| 	// err:
 | ||||
| } | ||||
| 
 | ||||
| func ExampleExecAndStreamError() { | ||||
| 	ExecAndStream("/", "man", []string{"Hello"}) | ||||
| 	// Output:
 | ||||
| 	// cmd.Run() failed with exit status 16
 | ||||
| 	//
 | ||||
| 	// out:
 | ||||
| 	//
 | ||||
| 	// err:
 | ||||
| 	// No manual entry for Hello
 | ||||
| } | ||||
| 
 | ||||
| func TestExecAndGetLines(t *testing.T) { | ||||
| 	testCommand := "echo" | ||||
| 	testArguments := []string{"$'Line1\nLine2\nLine3'"} | ||||
| 	var result []string | ||||
| 
 | ||||
| 	scanner := ExecAndGetLines("/", testCommand, testArguments) | ||||
| 
 | ||||
| 	for scanner.Scan() { | ||||
| 		result = append(result, scanner.Text()) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(result) != 3 { | ||||
| 		t.Log("Expected 3 lines but got ", len(result)) | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestDeleteEmptySlices(t *testing.T) { | ||||
| 	testSlice := []string{"Line1", "", "Line3"} | ||||
| 
 | ||||
| 	resultSlice := DeleteEmptySlices(testSlice) | ||||
| 
 | ||||
| 	if (len(resultSlice)) != 2 { | ||||
| 		t.Log("Expected 2 slices but got ", len(resultSlice)) | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func TestGetPath(t *testing.T) { | ||||
| 	invalidPathKey := "test" | ||||
| 
 | ||||
| 	result := GetPath(invalidPathKey) | ||||
| 	if result != "" { | ||||
| 		t.Log("Expected empty result but got ", result) | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 
 | ||||
| 	validPathKey := "wsPath" | ||||
| 	result = GetPath(validPathKey) | ||||
| 	if result != "/home/system/components/ws/" { | ||||
| 		t.Log("Expected /home/system/components/ws/ but got", result) | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetSQLiteFormattedDateTime(t *testing.T) { | ||||
| 	datetime := time.Date(2021, time.Month(1), 01, 1, 30, 15, 0, time.UTC) | ||||
| 	result := GetSQLiteFormattedDateTime(datetime) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue