mirror of
https://github.com/davidallendj/magellan.git
synced 2025-12-20 11:37:01 -07:00
Merge pull request 'Add cache command with interactive editor' (#15) from cache-cmd into main
Reviewed-on: towk/magellan-ng#15
This commit is contained in:
commit
193733a8e3
18 changed files with 884 additions and 194 deletions
41
README.md
41
README.md
|
|
@ -1,13 +1,16 @@
|
||||||
# Magellan
|
# Magellan (Next Generation)
|
||||||
|
|
||||||
The `magellan` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/davidallendj/smd/tree/master) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services.
|
The `magellan-ng` CLI tool is a Redfish-based, board management controller (BMC) discovery tool designed to scan networks and is written in Go. The tool collects information from BMC nodes using the provided Redfish RESTful API with [`gofish`](https://github.com/stmcginnis/gofish) and loads the queried data into an [SMD](https://github.com/davidallendj/smd/tree/master) instance. The tool strives to be more flexible by implementing multiple methods of discovery to work for a wider range of systems (WIP) and is capable of using independently of other tools or services.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The v0.1.0 version of `magellan` is incompatible with `smd` v2.15.3 and earlier due to `smd` lacking the inventory parsing code used with `magellan`'s output.**
|
> The v0.1.0 version of `magellan` is incompatible with `smd` v2.15.3 and earlier due to `smd` lacking the inventory parsing code used with `magellan`'s output.**
|
||||||
|
|
||||||
|
> [!NOTICE]
|
||||||
|
> This is the enhanced version of the original OpenCHAMI tool that includes more features, refactoring, and bug fixes. This fork allows for more unconstrained experimentation that would not be possible in the original version. Some of the features added to this fork may be added to the original version in the future.
|
||||||
|
|
||||||
<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->
|
<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->
|
||||||
|
|
||||||
- [Magellan](#magellan)
|
- [Magellan (Next Generation)](#magellan-next-generation)
|
||||||
- [Main Features](#main-features)
|
- [Main Features](#main-features)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Building the Executable](#building-the-executable)
|
- [Building the Executable](#building-the-executable)
|
||||||
|
|
@ -22,7 +25,8 @@ The `magellan` CLI tool is a Redfish-based, board management controller (BMC) di
|
||||||
- [Managing Secrets](#managing-secrets-1)
|
- [Managing Secrets](#managing-secrets-1)
|
||||||
- [Starting the Emulator](#starting-the-emulator-1)
|
- [Starting the Emulator](#starting-the-emulator-1)
|
||||||
- [Updating Firmware](#updating-firmware)
|
- [Updating Firmware](#updating-firmware)
|
||||||
- [Getting an Access Token (WIP)](#getting-an-access-token-wip)
|
- [Session Authentication](#session-authentication)
|
||||||
|
- [Cache Management](#cache-management)
|
||||||
- [Running with Docker](#running-with-docker)
|
- [Running with Docker](#running-with-docker)
|
||||||
- [How It Works](#how-it-works)
|
- [How It Works](#how-it-works)
|
||||||
- [TODO](#todo)
|
- [TODO](#todo)
|
||||||
|
|
@ -30,7 +34,7 @@ The `magellan` CLI tool is a Redfish-based, board management controller (BMC) di
|
||||||
|
|
||||||
<!-- TOC end -->
|
<!-- TOC end -->
|
||||||
|
|
||||||
<!-- TOC --><a name="openchami-magellan"></a>
|
<!-- TOC --><a name="magellan-ng"></a>
|
||||||
|
|
||||||
## Main Features
|
## Main Features
|
||||||
|
|
||||||
|
|
@ -43,6 +47,13 @@ The `magellan` tool comes packed with a handleful of features for doing discover
|
||||||
- Write inventory data to JSON
|
- Write inventory data to JSON
|
||||||
- Store and manage BMC secrets
|
- Store and manage BMC secrets
|
||||||
|
|
||||||
|
Some features are unique to `magellan-ng` that the original version does not have, including:
|
||||||
|
|
||||||
|
- Session authentication
|
||||||
|
- Cache management
|
||||||
|
- Storage included in inventory
|
||||||
|
- More robust scanning
|
||||||
|
|
||||||
See the [TODO](#todo) section for a list of soon-ish goals planned.
|
See the [TODO](#todo) section for a list of soon-ish goals planned.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
@ -441,25 +452,13 @@ Then, the update status can be viewed by including the `--status` flag along wit
|
||||||
watch -n 1 "./magellan update 172.16.0.110 --status --username $bmc_username --password $bmc_password | jq '.'"
|
watch -n 1 "./magellan update 172.16.0.110 --status --username $bmc_username --password $bmc_password | jq '.'"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting an Access Token (WIP)
|
### Session Authentication
|
||||||
|
|
||||||
The `magellan` tool has a `login` subcommand that works with the [`opaal`](https://github.com/davidallendj/opaal) service to obtain a token needed to access the SMD service. If the SMD instance requires authentication, set the `ACCESS_TOKEN` environment variable to have `magellan` include it in the header for HTTP requests to SMD.
|
TBD
|
||||||
|
|
||||||
```bash
|
### Cache Management
|
||||||
# must have a running OPAAL instance
|
|
||||||
./magellan login --url https://opaal:4444/login
|
|
||||||
|
|
||||||
# ...complete login flow to get token
|
TBD
|
||||||
export ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIs...
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, if you are running the davidallendj quickstart in the [deployment recipes](https://github.com/davidallendj/deployment-recipes), you can run the provided script to generate a token and set the environment variable that way.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
quickstart_dir=path/to/deployment/recipes/quickstart
|
|
||||||
source $quickstart_dir/bash_functions.sh
|
|
||||||
export ACCESS_TOKEN=$(gen_access_token)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running with Docker
|
### Running with Docker
|
||||||
|
|
||||||
|
|
|
||||||
316
cmd/cache.go
316
cmd/cache.go
|
|
@ -2,95 +2,303 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
cache "github.com/davidallendj/magellan/internal/cache"
|
||||||
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
||||||
|
"github.com/davidallendj/magellan/internal/util"
|
||||||
magellan "github.com/davidallendj/magellan/pkg"
|
magellan "github.com/davidallendj/magellan/pkg"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
withHosts []string
|
timestampf string
|
||||||
withPorts []int
|
timestamp time.Time
|
||||||
|
cacheOutputFormat string
|
||||||
|
interactive bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var cacheCmd = &cobra.Command{
|
var cacheCmd = &cobra.Command{
|
||||||
Use: "cache",
|
Use: "cache",
|
||||||
Short: "Manage found assets in cache.",
|
Short: "Manage assets (new, add, remove, edit, etc.) in cache.",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
// show the help for cache and exit
|
|
||||||
if len(args) <= 0 {
|
|
||||||
cmd.Help()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cacheNewCmd = &cobra.Command{
|
||||||
|
Use: "new",
|
||||||
|
Example: ` // create a new cache database
|
||||||
|
magellan cache new ./path/to/new_cache.db
|
||||||
|
`,
|
||||||
|
Short: "Create a new cache at specified path.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheAddCmd = &cobra.Command{
|
||||||
|
Use: "add [host] [port] [protocol] <timestamp>",
|
||||||
|
Example: ` // create a new entry in cache
|
||||||
|
magellan cache add https://172.16.0.105 443,5000 tcp "06/25/2025"
|
||||||
|
|
||||||
|
// create new entry with no timestamp specified with specific cache
|
||||||
|
magellan cache add https://172.16.0.111 443 tcp --cache ./assets.db
|
||||||
|
`,
|
||||||
|
Short: "Add an entry to the specified cache.",
|
||||||
|
Args: cobra.RangeArgs(3, 4),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
// set positional args to vars
|
||||||
|
host = args[0]
|
||||||
|
protocol = args[2]
|
||||||
|
|
||||||
|
// convert ports
|
||||||
|
for _, portStr := range strings.Split(args[1], ",") {
|
||||||
|
port, err := strconv.ParseInt(portStr, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to convert port value")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ports = append(ports, int(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse timestamp
|
||||||
|
if len(args) == 4 {
|
||||||
|
var err error
|
||||||
|
timestamp, err = dateparse.ParseAny(args[3])
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to parse time arg")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warn().Msg("time not specified...using current time")
|
||||||
|
timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
// create a new entry in cache using flag arguments
|
||||||
|
err := sqlite.InsertRemoteAssets(cachePath, magellan.RemoteAsset{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
Protocol: protocol,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("path", cachePath).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("failed to insert asset into cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheRemoveCmd = &cobra.Command{
|
var cacheRemoveCmd = &cobra.Command{
|
||||||
Use: "remove",
|
Use: "remove [hosts...]",
|
||||||
Short: "Remove a host from a scanned cache list.",
|
Example: ` // remove an entry in cache
|
||||||
|
magellan cache remove https://172.16.0.113
|
||||||
|
`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Short: "Remove hosts from cache.",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
assets := []magellan.RemoteAsset{}
|
err := sqlite.DeleteRemoteAssetsByHost(cachePath, args...)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", cachePath).Msg("failed to delete assets in cache")
|
||||||
|
}
|
||||||
|
|
||||||
// add all assets directly from positional args
|
},
|
||||||
for _, arg := range args {
|
}
|
||||||
|
|
||||||
|
var cacheEditCmd = &cobra.Command{
|
||||||
|
Use: "edit",
|
||||||
|
Example: ` // star the cache editor
|
||||||
|
magellan cache edit -i
|
||||||
|
|
||||||
|
// edit a single entry only changing values specified (e.g. port and protocol)
|
||||||
|
magellan cache edit https://172.16.0.101 --port 443 --protocol udp
|
||||||
|
|
||||||
|
// edit two entries' time stamps
|
||||||
|
magellan cache edit https://172.16.0.101 https://172.16.0.102 --timestamp 06/25/2025
|
||||||
|
`,
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if interactive {
|
||||||
|
// must have no args if interactive
|
||||||
|
if err := cobra.ExactArgs(0)(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// must have at least one arg if not interactive
|
||||||
|
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Short: "Edit existing cache data.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
var (
|
var (
|
||||||
port int
|
columns []table.Column
|
||||||
uri *url.URL
|
rows []table.Row
|
||||||
err error
|
styles table.Styles
|
||||||
)
|
)
|
||||||
uri, err = url.ParseRequestURI(arg)
|
|
||||||
|
if interactive {
|
||||||
|
// load the assets found from scan
|
||||||
|
scannedResults, err := sqlite.GetRemoteAssets(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to parse arg")
|
log.Error().Err(err).Str("path", cachePath).Msg("failed to get scanned assets from cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert port to its "proper" type
|
// set columns to cache headers
|
||||||
if uri.Port() == "" {
|
columns = []table.Column{
|
||||||
uri.Host += ":443"
|
{Title: "host", Width: 30},
|
||||||
}
|
{Title: "ports", Width: 8},
|
||||||
port, err = strconv.Atoi(uri.Port())
|
{Title: "protocol", Width: 10},
|
||||||
if err != nil {
|
{Title: "timestamp", Width: 20},
|
||||||
log.Error().Err(err).Msg("failed to convert port to integer type")
|
|
||||||
}
|
|
||||||
asset := magellan.RemoteAsset{
|
|
||||||
Host: fmt.Sprintf("%s://%s", uri.Scheme, uri.Hostname()),
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
assets = append(assets, asset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all assets with specified hosts (same host different different ports)
|
// set rows to cache data
|
||||||
// This should produce the following SQL:
|
for _, asset := range scannedResults {
|
||||||
// DELETE FROM magellan_scanned_assets WHERE host=:host
|
rows = append(rows, table.Row{
|
||||||
for _, host := range withHosts {
|
asset.Host,
|
||||||
assets = append(assets, magellan.RemoteAsset{
|
fmt.Sprintf("%d", asset.Port),
|
||||||
Host: host,
|
asset.Protocol,
|
||||||
Port: -1,
|
fmt.Sprintf("%d", asset.Timestamp.Unix()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Add all assets with specified ports (same port different hosts)
|
|
||||||
// This should produce the following SQL:
|
// create a new table
|
||||||
// DELETE FROM magellan_scanned_assets WHERE port=:port
|
assetsTable := table.New(
|
||||||
for _, port := range withPorts {
|
table.WithColumns(columns),
|
||||||
assets = append(assets, magellan.RemoteAsset{
|
table.WithRows(rows),
|
||||||
Host: "",
|
table.WithFocused(true),
|
||||||
Port: port,
|
table.WithHeight(10),
|
||||||
})
|
)
|
||||||
}
|
|
||||||
if len(assets) <= 0 {
|
// set up table styling
|
||||||
log.Error().Msg("nothing to do")
|
styles = table.DefaultStyles()
|
||||||
|
styles.Header = styles.Header.
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240")).
|
||||||
|
BorderBottom(true).
|
||||||
|
Bold(false)
|
||||||
|
styles.Selected = styles.Selected.
|
||||||
|
Foreground(lipgloss.Color("229")).
|
||||||
|
Background(lipgloss.Color("57")).
|
||||||
|
Bold(false)
|
||||||
|
assetsTable.SetStyles(styles)
|
||||||
|
|
||||||
|
m := cache.NewModel(cachePath, &assetsTable)
|
||||||
|
if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
|
||||||
|
fmt.Println("Error running program:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
sqlite.DeleteScannedAssets(cachePath, assets...)
|
} else {
|
||||||
|
// non-interactive editting
|
||||||
|
for _, host := range args {
|
||||||
|
// get the asset from cache for host
|
||||||
|
asset, err := sqlite.GetRemoteAsset(cachePath, host)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", cachePath).
|
||||||
|
Msg("failed to get asset from cache")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if asset == nil {
|
||||||
|
log.Warn().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", cachePath).
|
||||||
|
Msg("found asset is not valid")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// only modify values that are set
|
||||||
|
if host != "" {
|
||||||
|
asset.Host = host
|
||||||
|
}
|
||||||
|
if protocol != "" {
|
||||||
|
asset.Protocol = protocol
|
||||||
|
}
|
||||||
|
if timestampf != "" {
|
||||||
|
newTimestamp, err := dateparse.ParseAny(timestampf)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to parse timestamp value")
|
||||||
|
} else {
|
||||||
|
asset.Timestamp = newTimestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reinsert the asset into cache for each port
|
||||||
|
for _, port := range ports {
|
||||||
|
newAsset := *asset
|
||||||
|
newAsset.Port = port
|
||||||
|
err = sqlite.DeleteRemoteAssetsByHost(cachePath, host)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", cachePath).
|
||||||
|
Msg("failed to delete asset in cache")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = sqlite.InsertRemoteAssets(cachePath, newAsset)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Str("path", cachePath).
|
||||||
|
Msg("failed to re-insert asset into cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheInfoCmd = &cobra.Command{
|
||||||
|
Use: "info",
|
||||||
|
Short: "Show cache-related information and exit.",
|
||||||
|
Example: ` magellan cache info`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
printCacheInfo(cacheOutputFormat)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cacheRemoveCmd.Flags().StringSliceVar(&withHosts, "with-hosts", []string{}, "Remove all assets with specified hosts")
|
cacheEditCmd.Flags().StringVar(&host, "host", "", "Set the new host value.")
|
||||||
cacheRemoveCmd.Flags().IntSliceVar(&withPorts, "with-ports", []int{}, "Remove all assets with specified ports")
|
cacheEditCmd.Flags().IntSliceVar(&ports, "port", nil, "Set the new port values as comma-separated list.")
|
||||||
cacheCmd.AddCommand(cacheRemoveCmd)
|
cacheEditCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the new scheme value. (default is 'https')")
|
||||||
|
cacheEditCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the new protocol value. (default is 'tcp')")
|
||||||
|
cacheEditCmd.Flags().StringVar(×tampf, "timestamp", "", "Set the new timestamp value.")
|
||||||
|
cacheEditCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Start an interactive TUI to edit cache data")
|
||||||
|
|
||||||
|
cacheInfoCmd.Flags().StringVarP(&cacheOutputFormat, "format", "F", FORMAT_LIST, "Set the output format (list|json|yaml)")
|
||||||
|
|
||||||
|
cacheCmd.AddCommand(
|
||||||
|
cacheNewCmd,
|
||||||
|
cacheAddCmd,
|
||||||
|
cacheRemoveCmd,
|
||||||
|
cacheEditCmd,
|
||||||
|
cacheInfoCmd,
|
||||||
|
ListCmd, // magellan cache list (alias for 'magellan list')
|
||||||
|
)
|
||||||
rootCmd.AddCommand(cacheCmd)
|
rootCmd.AddCommand(cacheCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printCacheInfo(format string) {
|
||||||
|
assets, err := sqlite.GetRemoteAssets(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", cachePath).Msg("failed to get assets to print cache info")
|
||||||
|
}
|
||||||
|
cacheData := map[string]any{
|
||||||
|
"path": cachePath,
|
||||||
|
"assets": len(assets),
|
||||||
|
}
|
||||||
|
util.PrintMapWithFormat(cacheData, format)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ var CollectCmd = &cobra.Command{
|
||||||
Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\nSee the 'scan' command on how to perform a scan.",
|
Long: "Send request(s) to a collection of hosts running Redfish services found stored from the 'scan' in cache.\nSee the 'scan' command on how to perform a scan.",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// get probe states stored in db from scan
|
// get probe states stored in db from scan
|
||||||
scannedResults, err := sqlite.GetScannedAssets(cachePath)
|
scannedResults, err := sqlite.GetRemoteAssets(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("failed to get scanned results from cache")
|
log.Error().Err(err).Msgf("failed to get scanned results from cache")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ var CrawlCmd = &cobra.Command{
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
// Validate that the only argument is a valid URI
|
// Validate that the only argument is a valid URI
|
||||||
var err error
|
var err error
|
||||||
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
|
if err = cobra.ExactArgs(1)(cmd, args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
args[0], err = urlx.Sanitize(args[0])
|
args[0], err = urlx.Sanitize(args[0])
|
||||||
|
|
|
||||||
37
cmd/list.go
37
cmd/list.go
|
|
@ -1,11 +1,9 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
||||||
urlx "github.com/davidallendj/magellan/internal/urlx"
|
urlx "github.com/davidallendj/magellan/internal/urlx"
|
||||||
|
|
@ -13,7 +11,6 @@ import (
|
||||||
"github.com/davidallendj/magellan/pkg/crawler"
|
"github.com/davidallendj/magellan/pkg/crawler"
|
||||||
"github.com/davidallendj/magellan/pkg/secrets"
|
"github.com/davidallendj/magellan/pkg/secrets"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -42,41 +39,18 @@ var ListCmd = &cobra.Command{
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// check if we just want to show cache-related info and exit
|
// check if we just want to show cache-related info and exit
|
||||||
if showCacheInfo {
|
if showCacheInfo {
|
||||||
magellan.PrintMapWithFormat(map[string]any{
|
printCacheInfo(listOutputFormat)
|
||||||
"path": cachePath,
|
|
||||||
}, listOutputFormat)
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the assets found from scan
|
// load the assets found from scan
|
||||||
scannedResults, err := sqlite.GetScannedAssets(cachePath)
|
scannedResults, err := sqlite.GetRemoteAssets(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("path", cachePath).Msg("failed to get scanned assets from cache")
|
log.Error().Err(err).Str("path", cachePath).Msg("failed to get scanned assets from cache")
|
||||||
}
|
}
|
||||||
switch strings.ToLower(listOutputFormat) {
|
|
||||||
case FORMAT_JSON:
|
|
||||||
b, err := json.Marshal(scannedResults)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msgf("failed to unmarshal cached data to JSON")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\n", string(b))
|
|
||||||
case FORMAT_YAML:
|
|
||||||
b, err := yaml.Marshal(scannedResults)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msgf("failed to unmarshal cached data to YAML")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\n", string(b))
|
|
||||||
case FORMAT_LIST:
|
|
||||||
for _, r := range scannedResults {
|
|
||||||
fmt.Printf("%s:%d (%s) @%s\n", r.Host, r.Port, r.Protocol, r.Timestamp.Format(time.UnixDate))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Error().Msg("unrecognized format")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print cache data in specified format
|
// print cache data in specified format
|
||||||
magellan.PrintRemoteAssets(scannedResults, listOutputFormat)
|
magellan.PrintRemoteAssets(scannedResults, strings.ToLower(listOutputFormat))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,18 +87,19 @@ var listDrivesCmd = &cobra.Command{
|
||||||
log.Error().Err(err).Msg("failed to get drives")
|
log.Error().Err(err).Msg("failed to get drives")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
magellan.PrintDrives(drives)
|
magellan.PrintDrives(drives, listOutputFormat)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ListCmd.Flags().StringVarP(&listOutputFormat, "format", "F", "none", "Set the output format (list|json|yaml)")
|
ListCmd.Flags().StringVarP(&listOutputFormat, "format", "F", FORMAT_LIST, "Set the output format (list|json|yaml)")
|
||||||
ListCmd.Flags().BoolVar(&showCacheInfo, "cache-info", false, "Alias for 'magellan cache info'")
|
ListCmd.Flags().BoolVar(&showCacheInfo, "cache-info", false, "Alias for 'magellan cache info'")
|
||||||
|
|
||||||
listDrivesCmd.Flags().StringVarP(&listUsername, "username", "u", "", "Set the username for BMC login")
|
listDrivesCmd.Flags().StringVarP(&listUsername, "username", "u", "", "Set the username for BMC login")
|
||||||
listDrivesCmd.Flags().StringVarP(&listPassword, "password", "p", "", "Set the password for BMC login")
|
listDrivesCmd.Flags().StringVarP(&listPassword, "password", "p", "", "Set the password for BMC login")
|
||||||
listDrivesCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS verification")
|
listDrivesCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS verification")
|
||||||
listDrivesCmd.Flags().StringVarP(&secretsFile, "secrets-file", "f", "secrets.json", "Set the path to secrets store file to store credentials")
|
listDrivesCmd.Flags().StringVarP(&secretsFile, "secrets-file", "f", "secrets.json", "Set the path to secrets store file to store credentials")
|
||||||
|
listDrivesCmd.Flags().StringVarP(&listOutputFormat, "format", "F", FORMAT_LIST, "Set the output format (list|json|yaml)")
|
||||||
|
|
||||||
ListCmd.AddCommand(listDrivesCmd)
|
ListCmd.AddCommand(listDrivesCmd)
|
||||||
rootCmd.AddCommand(ListCmd)
|
rootCmd.AddCommand(ListCmd)
|
||||||
|
|
|
||||||
37
cmd/root.go
37
cmd/root.go
|
|
@ -61,6 +61,43 @@ var (
|
||||||
// a help message and then exits.
|
// a help message and then exits.
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "magellan",
|
Use: "magellan",
|
||||||
|
Example: ` // a typical dynamic work flow with unknown hosts
|
||||||
|
magellan scan --subnet 172.16.0.0/24
|
||||||
|
magellan collect -v -F yaml -o nodes.yaml -u admin -p password123
|
||||||
|
$EDITOR nodes.yaml
|
||||||
|
magellan send -F yaml -d@nodes.yaml https://api.example.com
|
||||||
|
magellan collect -v | magellan send
|
||||||
|
|
||||||
|
// show information in list
|
||||||
|
magellan list -F yaml
|
||||||
|
magellan list drives https://172.21.0.2:5000 -i -u admin -p password123
|
||||||
|
|
||||||
|
// performing a crawl with known hosts
|
||||||
|
magellan crawl https://172.16.0.100 -u admin -p password123 -i -F yaml -o nodes.yaml
|
||||||
|
|
||||||
|
// manage secrets with secret store
|
||||||
|
export MASTER_KEY=(magellan secrets generatekey)
|
||||||
|
magellan secrets store https://172.16.0.100 root:password123
|
||||||
|
magellan secrets list
|
||||||
|
magellan secrets retrieve https://172.16.0.100
|
||||||
|
magellan secrets remove https://172.16.0.100
|
||||||
|
|
||||||
|
// manage sessions with session authentication (can be used with secret store)
|
||||||
|
magellan sessions login https://172.16.0.100 -u root -p password123
|
||||||
|
magellan sessions list
|
||||||
|
magellan sessions delete --session-id $SESSION_ID --session-token $SESSION_TOKEN
|
||||||
|
|
||||||
|
// manage cache with cache editting
|
||||||
|
magellan cache new /tmp/magellan/$USER/assets.db
|
||||||
|
magellan cache add https://172.16.0.100 443 tcp
|
||||||
|
magellan cache remove https://172.16.0.100
|
||||||
|
magellan cache list
|
||||||
|
magellan cache info
|
||||||
|
|
||||||
|
// manage cache interactively with editor
|
||||||
|
magellan cache new ./my/new/cache.db
|
||||||
|
magellan cache edit -i --cache ./my/new/cache.db
|
||||||
|
`,
|
||||||
Short: "Redfish-based BMC discovery tool",
|
Short: "Redfish-based BMC discovery tool",
|
||||||
Long: "Redfish-based BMC discovery tool with dynamic discovery features.",
|
Long: "Redfish-based BMC discovery tool with dynamic discovery features.",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ var ScanCmd = &cobra.Command{
|
||||||
// TODO: change this to use an extensible plugin system for storage solutions
|
// TODO: change this to use an extensible plugin system for storage solutions
|
||||||
// (i.e. something like cache.InsertScannedAssets(path, assets) which implements a Cache interface)
|
// (i.e. something like cache.InsertScannedAssets(path, assets) which implements a Cache interface)
|
||||||
if len(foundAssets) > 0 {
|
if len(foundAssets) > 0 {
|
||||||
err = sqlite.InsertScannedAssets(cachePath, foundAssets...)
|
err = sqlite.InsertRemoteAssets(cachePath, foundAssets...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to write scanned assets to cache")
|
log.Error().Err(err).Msg("failed to write scanned assets to cache")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -18,6 +18,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.4
|
github.com/charmbracelet/bubbletea v1.3.4
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
|
@ -33,6 +34,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
|
|
|
||||||
8
go.sum
8
go.sum
|
|
@ -2,6 +2,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Cray-HPE/hms-xname v1.3.0 h1:DQmetMniubqcaL6Cxarz9+7KFfWGSEizIhfPHIgC3Gw=
|
github.com/Cray-HPE/hms-xname v1.3.0 h1:DQmetMniubqcaL6Cxarz9+7KFfWGSEizIhfPHIgC3Gw=
|
||||||
github.com/Cray-HPE/hms-xname v1.3.0/go.mod h1:XKdjQSzoTps5KDOE8yWojBTAWASGaS6LfRrVDxwTQO8=
|
github.com/Cray-HPE/hms-xname v1.3.0/go.mod h1:XKdjQSzoTps5KDOE8yWojBTAWASGaS6LfRrVDxwTQO8=
|
||||||
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||||
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
|
|
@ -70,6 +74,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
|
@ -92,6 +97,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
|
@ -105,6 +111,7 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
|
@ -124,6 +131,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
|
|
||||||
1
internal/cache/cache.go
vendored
1
internal/cache/cache.go
vendored
|
|
@ -8,6 +8,7 @@ import (
|
||||||
type Cache[T any] interface {
|
type Cache[T any] interface {
|
||||||
CreateIfNotExists(path string) (driver.Connector, error)
|
CreateIfNotExists(path string) (driver.Connector, error)
|
||||||
Insert(path string, data ...T) error
|
Insert(path string, data ...T) error
|
||||||
|
Update(path string, data ...T) error
|
||||||
Delete(path string, data ...T) error
|
Delete(path string, data ...T) error
|
||||||
Get(path string) ([]T, error)
|
Get(path string) ([]T, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
176
internal/cache/edit.go
vendored
Normal file
176
internal/cache/edit.go
vendored
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/cznic/mathutil"
|
||||||
|
"github.com/davidallendj/magellan/internal/cache/sqlite"
|
||||||
|
magellan "github.com/davidallendj/magellan/pkg"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Model) editSelectedRow(value bool) tea.Cmd {
|
||||||
|
if value {
|
||||||
|
row := m.Table.SelectedRow()
|
||||||
|
for i := range m.inputs {
|
||||||
|
m.inputs[i].SetValue(row[i])
|
||||||
|
}
|
||||||
|
m.Editor.StartEditting()
|
||||||
|
} else {
|
||||||
|
m.Editor.StopEditting()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) updateRow() tea.Cmd {
|
||||||
|
// get updated values from inputs
|
||||||
|
updated := make(table.Row, len(m.inputs))
|
||||||
|
for i, input := range m.inputs {
|
||||||
|
updated[i] = input.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// update table for selected row
|
||||||
|
rows := m.Table.Rows()
|
||||||
|
rows[m.Table.Cursor()] = updated
|
||||||
|
m.Table.SetRows(rows)
|
||||||
|
|
||||||
|
// go back to selecting view
|
||||||
|
m.Editor.StopEditting()
|
||||||
|
m.displayMessage(fmt.Sprintf("updated row at index %d", m.Table.Cursor()), 3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) addRowAfterCursor() tea.Cmd {
|
||||||
|
position := mathutil.Clamp(m.Table.Cursor()+1, 0, len(m.Table.Rows())-1)
|
||||||
|
rows := slices.Insert(m.Table.Rows(), position, table.Row{
|
||||||
|
"https://127.0.0.1",
|
||||||
|
"443",
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%d", time.Now().Unix()),
|
||||||
|
})
|
||||||
|
|
||||||
|
m.Table.SetRows(rows)
|
||||||
|
m.displayMessage(fmt.Sprintf("add new row at index %d", m.Table.Cursor()+1), 3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) addRowAtEnd() tea.Cmd {
|
||||||
|
rows := append(m.Table.Rows(), table.Row{
|
||||||
|
"https://127.0.0.1",
|
||||||
|
"443",
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%d", time.Now().Unix()),
|
||||||
|
})
|
||||||
|
m.Table.SetRows(rows)
|
||||||
|
m.displayMessage(fmt.Sprintf("add new row at index %d", len(m.Table.Rows())), 3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) deleteSelectedRow() tea.Cmd {
|
||||||
|
if len(m.Table.Rows()) == 0 {
|
||||||
|
m.displayMessage("nothing to delete...", 3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.Table.SetRows(slices.Delete(m.Table.Rows(), m.Table.Cursor(), m.Table.Cursor()+1))
|
||||||
|
m.displayMessage(fmt.Sprintf("deleted row at index %d", m.Table.Cursor()), 3)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) updateInputs(msg tea.Msg) tea.Cmd {
|
||||||
|
cmds := make([]tea.Cmd, len(m.inputs))
|
||||||
|
|
||||||
|
// Only text inputs with Focus() set will respond, so it's safe to simply
|
||||||
|
// update all of them here without any further logic.
|
||||||
|
for i := range m.inputs {
|
||||||
|
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) editRowView() string {
|
||||||
|
display := fmt.Sprintf("Editting row %d...\n\n", m.Table.Cursor())
|
||||||
|
|
||||||
|
// update the values of each input
|
||||||
|
const lc = 9
|
||||||
|
for i := range m.inputs {
|
||||||
|
diff := lc - len(m.Table.Columns()[i].Title)
|
||||||
|
spacing := strings.Repeat(" ", diff)
|
||||||
|
// m.inputs[i].SetValue(row[i])
|
||||||
|
display += fmt.Sprintf("%d: ", i)
|
||||||
|
display += m.Table.Columns()[i].Title + ": "
|
||||||
|
display += spacing + m.inputs[i].View() + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// submit button
|
||||||
|
// button := &blurredButton
|
||||||
|
// if m.focusIndex == len(m.inputs) {
|
||||||
|
// button = &focusedButton
|
||||||
|
// }
|
||||||
|
// display += fmt.Sprintf("\n\n%s\n\n", *button)
|
||||||
|
|
||||||
|
// add helper info in footer
|
||||||
|
display += footerStyle.Render(`
|
||||||
|
⬇/⬆: move cursor;
|
||||||
|
w, enter: save edit; esc: cancel; ctrl+c: quit w/o saving;
|
||||||
|
`)
|
||||||
|
|
||||||
|
return display
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) updateCacheData() tea.Cmd {
|
||||||
|
// create assets from table data
|
||||||
|
assets := []magellan.RemoteAsset{}
|
||||||
|
for _, row := range m.Table.Rows() {
|
||||||
|
// convert port
|
||||||
|
port, err := strconv.ParseInt(row[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("host", row[0]).Msg("failed to parse port value")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse timestamp
|
||||||
|
timestamp, err := dateparse.ParseAny(row[3])
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("host", row[0]).Msg("failed to parse date/time")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
assets = append(assets, magellan.RemoteAsset{
|
||||||
|
Host: row[0],
|
||||||
|
Port: int(port),
|
||||||
|
Protocol: row[2],
|
||||||
|
Timestamp: timestamp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove current database file
|
||||||
|
err := os.Remove(m.CachePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", m.CachePath).Msg("failed to remove old cache")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the file again...
|
||||||
|
_, err = sqlite.CreateRemoteAssetsIfNotExists(m.CachePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", m.CachePath).Msg("failed to create new cache")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write assets to database
|
||||||
|
err = sqlite.InsertRemoteAssets(m.CachePath, assets...)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("path", m.CachePath).Msg("failed to insert data into cache")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
1
internal/cache/edit/modify.go
vendored
1
internal/cache/edit/modify.go
vendored
|
|
@ -1 +0,0 @@
|
||||||
package cache
|
|
||||||
48
internal/cache/edit/table.go
vendored
48
internal/cache/edit/table.go
vendored
|
|
@ -1,48 +0,0 @@
|
||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charmbracelet/bubbles/table"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
var baseStyle = lipgloss.NewStyle().
|
|
||||||
BorderStyle(lipgloss.NormalBorder()).
|
|
||||||
BorderForeground(lipgloss.Color("240"))
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
selected int
|
|
||||||
Table table.Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd { return nil }
|
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
var cmd tea.Cmd
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
// m.Table = m.Table.Width(msg.Width)
|
|
||||||
// m.Table = m.Table.Height(msg.Height)
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "esc":
|
|
||||||
if m.Table.Focused() {
|
|
||||||
m.Table.Blur()
|
|
||||||
} else {
|
|
||||||
m.Table.Focus()
|
|
||||||
}
|
|
||||||
case "q", "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "enter":
|
|
||||||
return m, tea.Batch(
|
|
||||||
tea.Printf("Selected host '%s'", m.Table.SelectedRow()[0]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.Table, cmd = m.Table.Update(msg)
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) View() string {
|
|
||||||
return baseStyle.Render(m.Table.View()) + "\n"
|
|
||||||
}
|
|
||||||
67
internal/cache/sqlite/sqlite.go
vendored
67
internal/cache/sqlite/sqlite.go
vendored
|
|
@ -11,7 +11,17 @@ import (
|
||||||
|
|
||||||
const TABLE_NAME = "magellan_scanned_assets"
|
const TABLE_NAME = "magellan_scanned_assets"
|
||||||
|
|
||||||
func CreateScannedAssetIfNotExists(path string) (*sqlx.DB, error) {
|
func GetColumns() []string {
|
||||||
|
return []string{
|
||||||
|
"host",
|
||||||
|
"port",
|
||||||
|
"protocol",
|
||||||
|
"state",
|
||||||
|
"timestamp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRemoteAssetsIfNotExists(path string) (*sqlx.DB, error) {
|
||||||
schema := fmt.Sprintf(`
|
schema := fmt.Sprintf(`
|
||||||
CREATE TABLE IF NOT EXISTS %s (
|
CREATE TABLE IF NOT EXISTS %s (
|
||||||
host TEXT NOT NULL,
|
host TEXT NOT NULL,
|
||||||
|
|
@ -34,13 +44,13 @@ func CreateScannedAssetIfNotExists(path string) (*sqlx.DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InsertScannedAssets(path string, assets ...magellan.RemoteAsset) error {
|
func InsertRemoteAssets(path string, assets ...magellan.RemoteAsset) error {
|
||||||
if assets == nil {
|
if assets == nil {
|
||||||
return fmt.Errorf("states == nil")
|
return fmt.Errorf("states == nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// create database if it doesn't already exist
|
// create database if it doesn't already exist
|
||||||
db, err := CreateScannedAssetIfNotExists(path)
|
db, err := CreateRemoteAssetsIfNotExists(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +72,33 @@ func InsertScannedAssets(path string, assets ...magellan.RemoteAsset) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteScannedAssets(path string, assets ...magellan.RemoteAsset) error {
|
func DeleteRemoteAssetsByHost(path string, hosts ...string) error {
|
||||||
|
var (
|
||||||
|
db *sqlx.DB
|
||||||
|
tx *sqlx.Tx
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
db, err = sqlx.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
tx = db.MustBegin()
|
||||||
|
for _, host := range hosts {
|
||||||
|
sql := fmt.Sprintf(`DELETE FROM %s WHERE host='%s'`, TABLE_NAME, host)
|
||||||
|
_, err := tx.Exec(sql, &host)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to execute DELETE transaction: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRemoteAssets(path string, assets ...magellan.RemoteAsset) error {
|
||||||
var (
|
var (
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
tx *sqlx.Tx
|
tx *sqlx.Tx
|
||||||
|
|
@ -91,7 +127,7 @@ func DeleteScannedAssets(path string, assets ...magellan.RemoteAsset) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetScannedAssets(path string) ([]magellan.RemoteAsset, error) {
|
func GetRemoteAssets(path string) ([]magellan.RemoteAsset, error) {
|
||||||
// check if path exists first to prevent creating the database
|
// check if path exists first to prevent creating the database
|
||||||
_, exists := util.PathExists(path)
|
_, exists := util.PathExists(path)
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
@ -111,3 +147,24 @@ func GetScannedAssets(path string) ([]magellan.RemoteAsset, error) {
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRemoteAsset(path string, host string) (*magellan.RemoteAsset, error) {
|
||||||
|
// check if path exists first to prevent creating the database
|
||||||
|
_, exists := util.PathExists(path)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("no file found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// now check if the file is the SQLite database
|
||||||
|
db, err := sqlx.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []magellan.RemoteAsset{}
|
||||||
|
err = db.Select(&results, fmt.Sprintf("SELECT * FROM %s ORDER BY host ASC, port ASC;", TABLE_NAME))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve assets: %v", err)
|
||||||
|
}
|
||||||
|
return &results[0], nil
|
||||||
|
}
|
||||||
|
|
|
||||||
60
internal/cache/state.go
vendored
Normal file
60
internal/cache/state.go
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
type editorState interface{}
|
||||||
|
type selecting struct{}
|
||||||
|
type editting struct{}
|
||||||
|
|
||||||
|
type Editor struct {
|
||||||
|
state editorState
|
||||||
|
}
|
||||||
|
|
||||||
|
// transition between state
|
||||||
|
|
||||||
|
func NewEditor() Editor {
|
||||||
|
return Editor{state: selecting{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) StartEditting() editting {
|
||||||
|
e.state = editting{}
|
||||||
|
return e.state.(editting)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) StopEditting() selecting {
|
||||||
|
e.state = selecting{}
|
||||||
|
return e.state.(selecting)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s editting) FinishEditting(e *Editor) selecting {
|
||||||
|
e.state = selecting{}
|
||||||
|
return e.state.(selecting)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) GetState() editorState {
|
||||||
|
return e.state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) GetStateString() string {
|
||||||
|
switch e.state.(type) {
|
||||||
|
case selecting:
|
||||||
|
return "selecting"
|
||||||
|
case editting:
|
||||||
|
return "editting"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) IsEditting() bool {
|
||||||
|
return e.GetStateString() == "editting"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Editor) IsSelecting() bool {
|
||||||
|
return e.GetStateString() == "selecting"
|
||||||
|
}
|
||||||
|
|
||||||
|
func test() {
|
||||||
|
editor := NewEditor()
|
||||||
|
editting := editor.StartEditting()
|
||||||
|
|
||||||
|
// ...later on...
|
||||||
|
editting.FinishEditting(&editor)
|
||||||
|
}
|
||||||
194
internal/cache/table.go
vendored
Normal file
194
internal/cache/table.go
vendored
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/cznic/mathutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseStyle = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240"))
|
||||||
|
footerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||||
|
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||||
|
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||||
|
cursorStyle = focusedStyle
|
||||||
|
noStyle = lipgloss.NewStyle()
|
||||||
|
helpStyle = blurredStyle
|
||||||
|
cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
||||||
|
|
||||||
|
focusedButton = focusedStyle.Render("[ Update ]")
|
||||||
|
blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Update"))
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
CachePath string
|
||||||
|
Editor Editor
|
||||||
|
Table table.Model
|
||||||
|
|
||||||
|
// privates
|
||||||
|
inputs []textinput.Model
|
||||||
|
focusIndex int
|
||||||
|
statusMsg string
|
||||||
|
statusTimer time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel(cachePath string, table *table.Model) Model {
|
||||||
|
m := Model{
|
||||||
|
CachePath: cachePath,
|
||||||
|
Editor: NewEditor(),
|
||||||
|
Table: *table,
|
||||||
|
inputs: make([]textinput.Model, 4),
|
||||||
|
focusIndex: 0,
|
||||||
|
}
|
||||||
|
for i := range len(m.inputs) {
|
||||||
|
input := textinput.New()
|
||||||
|
input.Cursor.Style = cursorStyle
|
||||||
|
input.CharLimit = 256
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
input.Placeholder = "host"
|
||||||
|
input.Focus()
|
||||||
|
input.PromptStyle = focusedStyle
|
||||||
|
input.TextStyle = focusedStyle
|
||||||
|
case 1:
|
||||||
|
input.Placeholder = "ports"
|
||||||
|
input.CharLimit = 5
|
||||||
|
case 2:
|
||||||
|
input.Placeholder = "protocol"
|
||||||
|
input.CharLimit = 5
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
input.Placeholder = "timestamp"
|
||||||
|
input.CharLimit = 10
|
||||||
|
}
|
||||||
|
m.inputs[i] = input
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd { return nil }
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
// m.Table.SetWidth(msg.Width)
|
||||||
|
// m.Table.SetHeight(msg.Height)
|
||||||
|
case tea.KeyMsg:
|
||||||
|
// do always regardless of state
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
// do while editting table row
|
||||||
|
if m.Editor.IsEditting() {
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
return m, m.editSelectedRow(false)
|
||||||
|
case "w", "enter":
|
||||||
|
return m, m.updateRow()
|
||||||
|
// case "enter":
|
||||||
|
// // Did the user press enter while the submit button was focused?
|
||||||
|
// // If so, exit.
|
||||||
|
// if m.focusIndex == len(m.inputs) {
|
||||||
|
// return m, tea.Quit
|
||||||
|
// }
|
||||||
|
|
||||||
|
case "tab", "shift+tab", "up", "down":
|
||||||
|
s := msg.String()
|
||||||
|
switch s {
|
||||||
|
case "up", "shift+tab":
|
||||||
|
m.focusIndex = mathutil.Clamp(m.focusIndex-1, 0, len(m.inputs))
|
||||||
|
case "down", "tab":
|
||||||
|
m.focusIndex = mathutil.Clamp(m.focusIndex+1, 0, len(m.inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds := make([]tea.Cmd, len(m.inputs))
|
||||||
|
for i := range m.inputs {
|
||||||
|
if i == m.focusIndex {
|
||||||
|
// Set focused state
|
||||||
|
cmds[i] = m.inputs[i].Focus()
|
||||||
|
m.inputs[i].PromptStyle = focusedStyle
|
||||||
|
m.inputs[i].TextStyle = focusedStyle
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Remove focused state
|
||||||
|
m.inputs[i].Blur()
|
||||||
|
m.inputs[i].PromptStyle = noStyle
|
||||||
|
m.inputs[i].TextStyle = noStyle
|
||||||
|
}
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
cmd = m.updateInputs(msg)
|
||||||
|
return m, tea.Batch(cmd)
|
||||||
|
// do while making selection from table
|
||||||
|
} else if m.Editor.IsSelecting() {
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
return m, m.editSelectedRow(true)
|
||||||
|
case "esc":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "ctrl+w":
|
||||||
|
m.updateCacheData()
|
||||||
|
return m, tea.Quit
|
||||||
|
case "w", "ctrl+s":
|
||||||
|
return m, m.updateCacheData()
|
||||||
|
case "d":
|
||||||
|
return m, m.deleteSelectedRow()
|
||||||
|
case "a":
|
||||||
|
return m, m.addRowAfterCursor()
|
||||||
|
case "ctrl+a":
|
||||||
|
return m, m.addRowAtEnd()
|
||||||
|
case "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
m.Table, cmd = m.Table.Update(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() string {
|
||||||
|
if m.Editor.IsEditting() {
|
||||||
|
return m.editRowView()
|
||||||
|
} else if m.Editor.IsSelecting() {
|
||||||
|
display := m.Table.View() + "\n"
|
||||||
|
display += footerStyle.Render(fmt.Sprintf(`
|
||||||
|
j/k, ⬇/⬆ : move cursor; enter: choose;
|
||||||
|
a: add row after cursor; ctrl+a: add row at end
|
||||||
|
d: delete row at cursor;
|
||||||
|
w, ctrl+s: save data;
|
||||||
|
ctrl+w: save and quit; q, esc: quit w/o saving;
|
||||||
|
----------------------------------------------------------
|
||||||
|
cache: %s
|
||||||
|
cursor: %d, rows: %d
|
||||||
|
%s
|
||||||
|
`, m.CachePath, m.Table.Cursor(), len(m.Table.Rows()), m.statusMsg))
|
||||||
|
|
||||||
|
return baseStyle.Render(display + "\n")
|
||||||
|
}
|
||||||
|
return "Something went wrong..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) displayMessage(message string, timeout time.Duration) {
|
||||||
|
|
||||||
|
// show the message arg for alloted time
|
||||||
|
m.statusMsg = message
|
||||||
|
reset := make(chan string)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(timeout)
|
||||||
|
reset <- ""
|
||||||
|
}()
|
||||||
|
m.statusMsg = <-reset
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package util
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
@ -23,3 +24,22 @@ func PrintYAML(data any) {
|
||||||
}
|
}
|
||||||
fmt.Print(string(b))
|
fmt.Print(string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PrintMap(data map[string]any) {
|
||||||
|
for k, v := range data {
|
||||||
|
fmt.Printf("%s: %v\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintMapWithFormat(data map[string]any, format string) {
|
||||||
|
switch strings.ToLower(format) {
|
||||||
|
case "json":
|
||||||
|
PrintJSON(data)
|
||||||
|
case "yaml":
|
||||||
|
PrintYAML(data)
|
||||||
|
case "list":
|
||||||
|
PrintMap(data)
|
||||||
|
default:
|
||||||
|
log.Error().Msg("PrintMapWithFormat: unrecognized format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
48
pkg/list.go
48
pkg/list.go
|
|
@ -27,21 +27,6 @@ func PrintRemoteAssets(data []RemoteAsset, format string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintMapWithFormat(data map[string]any, format string) {
|
|
||||||
switch strings.ToLower(format) {
|
|
||||||
case "json":
|
|
||||||
util.PrintJSON(data)
|
|
||||||
case "yaml":
|
|
||||||
util.PrintYAML(data)
|
|
||||||
case "list":
|
|
||||||
for k, v := range data {
|
|
||||||
fmt.Printf("%s: %v\n", k, v)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Error().Msg("PrintMapWithFormat: unrecognized format")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListDrives(cc *crawler.CrawlerConfig) ([]*redfish.Drive, error) {
|
func ListDrives(cc *crawler.CrawlerConfig) ([]*redfish.Drive, error) {
|
||||||
user, err := cc.GetUserPass()
|
user, err := cc.GetUserPass()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -87,14 +72,31 @@ func ListDrives(cc *crawler.CrawlerConfig) ([]*redfish.Drive, error) {
|
||||||
return foundDrives, nil
|
return foundDrives, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintDrives(drives []*redfish.Drive) {
|
func PrintDrives(drives []*redfish.Drive, format string) {
|
||||||
|
switch format {
|
||||||
|
case "json":
|
||||||
|
util.PrintJSON(drives)
|
||||||
|
case "yaml":
|
||||||
|
util.PrintYAML(drives)
|
||||||
|
case "list":
|
||||||
for i, drive := range drives {
|
for i, drive := range drives {
|
||||||
fmt.Printf("Drive %d\n", i)
|
fmt.Printf(`
|
||||||
fmt.Printf("\tManufacturer: %s\n", drive.Manufacturer)
|
Drive %d
|
||||||
fmt.Printf("\tModel: %s\n", drive.Model)
|
\tManufacturuer: %s
|
||||||
fmt.Printf("\tSize: %d GiB\n", (drive.CapacityBytes / 1024 / 1024 / 1024))
|
\tModel: %s
|
||||||
fmt.Printf("\tSerial number: %s\n", drive.SerialNumber)
|
\tSize: %d GiB
|
||||||
fmt.Printf("\tPart number: %s\n", drive.PartNumber)
|
\tSerial Number: %s
|
||||||
fmt.Printf("\tLocation: %s %d\n", drive.PhysicalLocation.PartLocation.LocationType, drive.PhysicalLocation.PartLocation.LocationOrdinalValue)
|
\tPart Number: %s
|
||||||
|
\tLocation: %s,%s
|
||||||
|
`, i,
|
||||||
|
drive.Manufacturer,
|
||||||
|
drive.Model,
|
||||||
|
(drive.CapacityBytes / 1024 / 1024 / 1024),
|
||||||
|
drive.SerialNumber,
|
||||||
|
drive.PartNumber,
|
||||||
|
drive.PhysicalLocation.PartLocation.LocationType,
|
||||||
|
drive.PhysicalLocation.PartLocation.LocationOrdinalValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue