diff --git a/calnex/cmd/cmd.go b/calnex/cmd/cmd.go index 4b180e71..599b7117 100644 --- a/calnex/cmd/cmd.go +++ b/calnex/cmd/cmd.go @@ -36,6 +36,7 @@ var ( insecureTLS bool source string target string + saveConfig string ) // Execute is the main entry point for CLI interface diff --git a/calnex/cmd/config.go b/calnex/cmd/config.go index 630f58c5..b06d0291 100644 --- a/calnex/cmd/config.go +++ b/calnex/cmd/config.go @@ -32,12 +32,14 @@ func init() { configCmd.Flags().BoolVar(&insecureTLS, "insecureTLS", false, "Ignore TLS certificate errors") configCmd.Flags().StringVar(&target, "target", "", "device to configure") configCmd.Flags().StringVar(&source, "file", "", "configuration file") + configCmd.Flags().StringVar(&saveConfig, "save", "", "save configuration to the specified path") if err := configCmd.MarkFlagRequired("target"); err != nil { log.Fatal(err) } if err := configCmd.MarkFlagRequired("file"); err != nil { log.Fatal(err) } + configCmd.MarkFlagsMutuallyExclusive("apply", "save") } var configCmd = &cobra.Command{ @@ -65,8 +67,14 @@ var configCmd = &cobra.Command{ log.Fatalf("Failed to find config for %s in %s", target, source) } - if err := config.Config(target, insecureTLS, dc, apply); err != nil { - log.Fatal(err) + if saveConfig != "" { + if err := config.Save(target, insecureTLS, dc, saveConfig); err != nil { + log.Fatal(err) + } + } else { + if err := config.Config(target, insecureTLS, dc, apply); err != nil { + log.Fatal(err) + } } }, } diff --git a/calnex/config/config.go b/calnex/config/config.go index cb7f44cf..9df57158 100644 --- a/calnex/config/config.go +++ b/calnex/config/config.go @@ -193,20 +193,12 @@ func Config(target string, insecureTLS bool, cc *CalnexConfig, apply bool) error var c config api := api.NewAPI(target, insecureTLS) - f, err := api.FetchSettings() + f, err := prepare(&c, api, target, cc) + if err != nil { return err } - m := f.Section("measure") - g := f.Section("gnss") - - // set base config - c.baseConfig(m, g, target, cc.AntennaDelayNS) - - // set measure config - c.measureConfig(m, cc.Measure) - if !apply { log.Info("dry run. Exiting") return nil @@ -246,3 +238,41 @@ func Config(target string, insecureTLS bool, cc *CalnexConfig, apply bool) error return nil } + +// Save saves the Network/Calnex configs to file +func Save(target string, insecureTLS bool, cc *CalnexConfig, saveConfig string) error { + var c config + api := api.NewAPI(target, insecureTLS) + + f, err := prepare(&c, api, target, cc) + + if err != nil { + return err + } + + err = f.SaveTo(saveConfig) + + if err != nil { + return err + } + + return nil +} + +func prepare(c *config, api *api.API, target string, cc *CalnexConfig) (*ini.File, error) { + f, err := api.FetchSettings() + if err != nil { + return nil, err + } + + m := f.Section("measure") + g := f.Section("gnss") + + // set base config + c.baseConfig(m, g, target, cc.AntennaDelayNS) + + // set measure config + c.measureConfig(m, cc.Measure) + + return f, nil +} diff --git a/calnex/config/config_test.go b/calnex/config/config_test.go index 760908d9..be12d6a4 100644 --- a/calnex/config/config_test.go +++ b/calnex/config/config_test.go @@ -23,6 +23,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" @@ -649,3 +650,538 @@ func TestJSONExport(t *testing.T) { require.NoError(t, err) require.Equal(t, expected, string(jsonData)) } + +func TestSaveConfig(t *testing.T) { + expectedConfig := `[gnss] +antenna_delay = 42 ns + +[measure] +device_name = %s +continuous = On +reference = Internal +meas_time = 1 days 1 hours +tie_mode = TIE + 1 PPS Alignment +ch0\used = Yes +ch1\used = No +ch2\used = No +ch3\used = No +ch4\used = No +ch5\used = No +ch6\used = Yes +ch7\used = No +ch8\used = Yes +ch9\used = Yes +ch10\used = No +ch11\used = No +ch12\used = No +ch13\used = No +ch14\used = No +ch15\used = No +ch16\used = No +ch17\used = No +ch18\used = No +ch19\used = No +ch20\used = No +ch21\used = No +ch22\used = No +ch23\used = No +ch24\used = No +ch25\used = No +ch26\used = No +ch27\used = No +ch28\used = No +ch29\used = No +ch30\used = Yes +ch31\used = No +ch32\used = No +ch33\used = No +ch34\used = No +ch35\used = No +ch36\used = No +ch37\used = No +ch38\used = No +ch39\used = No +ch40\used = No +ch6\protocol_enabled = Off +ch7\protocol_enabled = Off +ch6\virtual_channels_enabled = On +ch9\protocol_enabled = On +ch9\ptp_synce\mode\probe_type = NTP +ch10\protocol_enabled = Off +ch10\ptp_synce\mode\probe_type = Disabled +ch11\protocol_enabled = Off +ch11\ptp_synce\mode\probe_type = Disabled +ch12\protocol_enabled = Off +ch12\ptp_synce\mode\probe_type = Disabled +ch13\protocol_enabled = Off +ch13\ptp_synce\mode\probe_type = Disabled +ch14\protocol_enabled = Off +ch14\ptp_synce\mode\probe_type = Disabled +ch15\protocol_enabled = Off +ch15\ptp_synce\mode\probe_type = Disabled +ch16\protocol_enabled = Off +ch16\ptp_synce\mode\probe_type = Disabled +ch17\protocol_enabled = Off +ch17\ptp_synce\mode\probe_type = Disabled +ch18\protocol_enabled = Off +ch18\ptp_synce\mode\probe_type = Disabled +ch19\protocol_enabled = Off +ch19\ptp_synce\mode\probe_type = Disabled +ch20\protocol_enabled = Off +ch20\ptp_synce\mode\probe_type = Disabled +ch21\protocol_enabled = Off +ch21\ptp_synce\mode\probe_type = Disabled +ch22\protocol_enabled = Off +ch22\ptp_synce\mode\probe_type = Disabled +ch23\protocol_enabled = Off +ch23\ptp_synce\mode\probe_type = Disabled +ch24\protocol_enabled = Off +ch24\ptp_synce\mode\probe_type = Disabled +ch25\protocol_enabled = Off +ch25\ptp_synce\mode\probe_type = Disabled +ch26\protocol_enabled = Off +ch26\ptp_synce\mode\probe_type = Disabled +ch27\protocol_enabled = Off +ch27\ptp_synce\mode\probe_type = Disabled +ch28\protocol_enabled = Off +ch28\ptp_synce\mode\probe_type = Disabled +ch29\protocol_enabled = Off +ch29\ptp_synce\mode\probe_type = Disabled +ch30\protocol_enabled = On +ch30\ptp_synce\mode\probe_type = PTP +ch31\protocol_enabled = Off +ch31\ptp_synce\mode\probe_type = Disabled +ch32\protocol_enabled = Off +ch32\ptp_synce\mode\probe_type = Disabled +ch33\protocol_enabled = Off +ch33\ptp_synce\mode\probe_type = Disabled +ch34\protocol_enabled = Off +ch34\ptp_synce\mode\probe_type = Disabled +ch35\protocol_enabled = Off +ch35\ptp_synce\mode\probe_type = Disabled +ch36\protocol_enabled = Off +ch36\ptp_synce\mode\probe_type = Disabled +ch37\protocol_enabled = Off +ch37\ptp_synce\mode\probe_type = Disabled +ch38\protocol_enabled = Off +ch38\ptp_synce\mode\probe_type = Disabled +ch39\protocol_enabled = Off +ch39\ptp_synce\mode\probe_type = Disabled +ch40\protocol_enabled = Off +ch40\ptp_synce\mode\probe_type = Disabled +ch0\server_ip = fd00:3226:301b::1f +ch0\signal_type = 1 PPS +ch0\trig_level = 500 mV +ch0\freq = 1 Hz +ch0\suppress_steps = Yes +ch6\synce_enabled = Off +ch6\ptp_synce\ptp\dscp = 0 +ch6\ptp_synce\ethernet\dhcp_v4 = Disabled +ch6\ptp_synce\ethernet\dhcp_v6 = DHCP +ch6\ptp_synce\ethernet\qsfp_fec = RS-FEC +ch7\synce_enabled = Off +ch7\ptp_synce\ptp\dscp = 0 +ch7\ptp_synce\ethernet\dhcp_v4 = Disabled +ch7\ptp_synce\ethernet\dhcp_v6 = DHCP +ch9\ptp_synce\ntp\server_ip = fd00:3226:301b::3f +ch9\ptp_synce\ntp\server_ip_ipv6 = fd00:3226:301b::3f +ch9\ptp_synce\physical_packet_channel = Channel 1 +ch9\ptp_synce\ntp\normalize_delays = Off +ch9\ptp_synce\ntp\protocol_level = UDP/IPv6 +ch9\ptp_synce\ntp\poll_log_interval = 1 packet/16 s +ch30\ptp_synce\ptp\version = SPTP_V2.1 +ch30\ptp_synce\ptp\master_ip = fd00:3016:3109:face:0:1:0 +ch30\ptp_synce\ptp\master_ip_ipv6 = fd00:3016:3109:face:0:1:0 +ch30\ptp_synce\physical_packet_channel = Channel 1 +ch30\ptp_synce\ptp\protocol_level = UDP/IPv6 +ch30\ptp_synce\ptp\log_announce_int = 1 packet/16 s +ch30\ptp_synce\ptp\log_delay_req_int = 1 packet/16 s +ch30\ptp_synce\ptp\log_sync_int = 1 packet/16 s +ch30\ptp_synce\ptp\stack_mode = Unicast +ch30\ptp_synce\ptp\domain = 0 +` + + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, + r *http.Request) { + fmt.Fprintln(w, "[measure]\nch0\\used=No\nch6\\used=Yes\nch9\\used=Yes\nch22\\used=Yes") + })) + defer ts.Close() + + parsed, _ := url.Parse(ts.URL) + calnexAPI := api.NewAPI(parsed.Host, true) + calnexAPI.Client = ts.Client() + expectedConfig = fmt.Sprintf(expectedConfig, parsed.Host) + cc := &CalnexConfig{ + AntennaDelayNS: 42, + Measure: map[api.Channel]MeasureConfig{ + api.ChannelA: { + Target: "fd00:3226:301b::1f", + Probe: api.ProbePPS, + }, + api.ChannelVP1: { + Target: "fd00:3226:301b::3f", + Probe: api.ProbeNTP, + }, + api.ChannelVP22: { + Target: "fd00:3016:3109:face:0:1:0", + Probe: api.ProbePTP, + }, + }, + } + + // Prepare tmp config file location + f, err := os.CreateTemp("/tmp", "calnex") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + err = Save(parsed.Host, true, cc, f.Name()) + require.NoError(t, err) + + savedConfig, err := os.ReadFile(f.Name()) + require.NoError(t, err) + + require.ElementsMatch(t, strings.Split(expectedConfig, "\n"), strings.Split(string(savedConfig), "\n")) +} + +func TestSaveConfigFail(t *testing.T) { + cc := &CalnexConfig{Measure: map[api.Channel]MeasureConfig{}} + + // Prepare tmp config file location + f, err := os.CreateTemp("/tmp", "calnex") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + + err = Save("localhost", true, cc, f.Name()) + require.Error(t, err) +} + +func TestPrepare(t *testing.T) { + testConfig := `[gnss] +antenna_delay=650 ns +[measure] +continuous=Off +reference=Auto +meas_time=10 minutes +tie_mode=TIE +ch6\used=No +ch8\used=Yes +ch0\used=Yes +ch1\used=Yes +ch2\used=No +ch3\used=Yes +ch4\used=No +ch5\used=Yes +ch7\used=Yes +ch9\used=No +ch10\used=No +ch11\used=Yes +ch12\used=No +ch13\used=No +ch14\used=No +ch15\used=No +ch16\used=No +ch17\used=No +ch18\used=Yes +ch19\used=No +ch20\used=No +ch21\used=No +ch22\used=Yes +ch23\used=No +ch24\used=No +ch25\used=Yes +ch26\used=No +ch27\used=No +ch28\used=No +ch29\used=No +ch30\used=Yes +ch31\used=No +ch32\used=Yes +ch33\used=No +ch34\used=No +ch35\used=No +ch36\used=No +ch37\used=No +ch38\used=Yes +ch39\used=No +ch40\used=No +ch6\protocol_enabled=Yes +ch6\ptp_synce\mode\probe_type=Disabled +ch7\protocol_enabled=Off +ch7\ptp_synce\mode\probe_type=Disabled +ch9\protocol_enabled=Off +ch9\ptp_synce\mode\probe_type=Disabled +ch10\protocol_enabled=Off +ch10\ptp_synce\mode\probe_type=Disabled +ch11\protocol_enabled=Off +ch11\ptp_synce\mode\probe_type=Disabled +ch12\protocol_enabled=Off +ch12\ptp_synce\mode\probe_type=Disabled +ch13\protocol_enabled=Off +ch13\ptp_synce\mode\probe_type=Disabled +ch14\protocol_enabled=Off +ch14\ptp_synce\mode\probe_type=Disabled +ch15\protocol_enabled=On +ch15\ptp_synce\mode\probe_type=Disabled +ch16\protocol_enabled=Off +ch16\ptp_synce\mode\probe_type=Disabled +ch17\protocol_enabled=Off +ch17\ptp_synce\mode\probe_type=Disabled +ch18\protocol_enabled=Off +ch18\ptp_synce\mode\probe_type=Disabled +ch19\protocol_enabled=Off +ch19\ptp_synce\mode\probe_type=Disabled +ch20\protocol_enabled=Off +ch20\ptp_synce\mode\probe_type=Disabled +ch21\protocol_enabled=Off +ch21\ptp_synce\mode\probe_type=Disabled +ch22\protocol_enabled=Off +ch22\ptp_synce\mode\probe_type=Disabled +ch23\protocol_enabled=Off +ch23\ptp_synce\mode\probe_type=Disabled +ch24\protocol_enabled=Off +ch24\ptp_synce\mode\probe_type=Disabled +ch25\protocol_enabled=On +ch25\ptp_synce\mode\probe_type=Disabled +ch26\protocol_enabled=Off +ch26\ptp_synce\mode\probe_type=NTP +ch27\protocol_enabled=Off +ch27\ptp_synce\mode\probe_type=Disabled +ch28\protocol_enabled=Off +ch28\ptp_synce\mode\probe_type=Disabled +ch29\protocol_enabled=Off +ch29\ptp_synce\mode\probe_type=PTP +ch30\protocol_enabled=On +ch30\ptp_synce\mode\probe_type=Disabled +ch31\protocol_enabled=Off +ch31\ptp_synce\mode\probe_type=Disabled +ch32\protocol_enabled=Off +ch32\ptp_synce\mode\probe_type=Disabled +ch33\protocol_enabled=Off +ch33\ptp_synce\mode\probe_type=Disabled +ch34\protocol_enabled=Off +ch34\ptp_synce\mode\probe_type=Disabled +ch35\protocol_enabled=Off +ch35\ptp_synce\mode\probe_type=Disabled +ch36\protocol_enabled=Off +ch36\ptp_synce\mode\probe_type=Disabled +ch37\protocol_enabled=Off +ch37\ptp_synce\mode\probe_type=Disabled +ch38\protocol_enabled=Off +ch38\ptp_synce\mode\probe_type=Disabled +ch39\protocol_enabled=Off +ch39\ptp_synce\mode\probe_type=Disabled +ch40\protocol_enabled=Off +ch40\ptp_synce\mode\probe_type=Disabled +ch0\server_ip=10.32.1.168 +ch0\signal_type=1 PPS +ch0\trig_level=1 V +ch0\freq=1 Hz +ch0\suppress_steps=No +ch9\ptp_synce\mode\probe_type=NTP +ch9\ptp_synce\ntp\server_ip=10.32.1.168 +ch9\ptp_synce\ntp\server_ip_ipv6=2000::000a +ch9\ptp_synce\physical_packet_channel=Channel 2 +ch9\ptp_synce\ntp\normalize_delays=On +ch9\ptp_synce\ntp\protocol_level=UDP/IPv4 +ch9\ptp_synce\ntp\poll_log_interval=1 packet/1 s +ch30\ptp_synce\mode\probe_type=PTP +ch30\ptp_synce\ptp\version=PTP_V2.1 +ch30\ptp_synce\ptp\master_ip=10.32.1.168 +ch30\ptp_synce\ptp\master_ip_ipv6=2000::000a +ch30\ptp_synce\physical_packet_channel=Channel 2 +ch30\ptp_synce\ptp\protocol_level=UDP/IPv4 +ch30\ptp_synce\ptp\log_announce_int=1 packet/1 s +ch30\ptp_synce\ptp\log_delay_req_int=1 packet/1 s +ch30\ptp_synce\ptp\log_sync_int=1 packet/1 s +ch30\ptp_synce\ptp\stack_mode=Multicast +ch30\ptp_synce\ptp\domain=1 +` + + expectedConfig := `[gnss] +antenna_delay=42 ns +[measure] +continuous=On +reference=Internal +meas_time=1 days 1 hours +tie_mode=TIE + 1 PPS Alignment +ch6\used=Yes +ch8\used=Yes +ch0\used=Yes +ch1\used=No +ch2\used=No +ch3\used=No +ch4\used=No +ch5\used=No +ch7\used=No +ch9\used=Yes +ch10\used=No +ch11\used=No +ch12\used=No +ch13\used=No +ch14\used=No +ch15\used=No +ch16\used=No +ch17\used=No +ch18\used=No +ch19\used=No +ch20\used=No +ch21\used=No +ch22\used=No +ch23\used=No +ch24\used=No +ch25\used=No +ch26\used=No +ch27\used=No +ch28\used=No +ch29\used=No +ch30\used=Yes +ch31\used=No +ch32\used=No +ch33\used=No +ch34\used=No +ch35\used=No +ch36\used=No +ch37\used=No +ch38\used=No +ch39\used=No +ch40\used=No +ch6\protocol_enabled=Off +ch6\ptp_synce\mode\probe_type=Disabled +ch7\protocol_enabled=Off +ch7\ptp_synce\mode\probe_type=Disabled +ch9\protocol_enabled=On +ch9\ptp_synce\mode\probe_type=NTP +ch10\protocol_enabled=Off +ch10\ptp_synce\mode\probe_type=Disabled +ch11\protocol_enabled=Off +ch11\ptp_synce\mode\probe_type=Disabled +ch12\protocol_enabled=Off +ch12\ptp_synce\mode\probe_type=Disabled +ch13\protocol_enabled=Off +ch13\ptp_synce\mode\probe_type=Disabled +ch14\protocol_enabled=Off +ch14\ptp_synce\mode\probe_type=Disabled +ch15\protocol_enabled=Off +ch15\ptp_synce\mode\probe_type=Disabled +ch16\protocol_enabled=Off +ch16\ptp_synce\mode\probe_type=Disabled +ch17\protocol_enabled=Off +ch17\ptp_synce\mode\probe_type=Disabled +ch18\protocol_enabled=Off +ch18\ptp_synce\mode\probe_type=Disabled +ch19\protocol_enabled=Off +ch19\ptp_synce\mode\probe_type=Disabled +ch20\protocol_enabled=Off +ch20\ptp_synce\mode\probe_type=Disabled +ch21\protocol_enabled=Off +ch21\ptp_synce\mode\probe_type=Disabled +ch22\protocol_enabled=Off +ch22\ptp_synce\mode\probe_type=Disabled +ch23\protocol_enabled=Off +ch23\ptp_synce\mode\probe_type=Disabled +ch24\protocol_enabled=Off +ch24\ptp_synce\mode\probe_type=Disabled +ch25\protocol_enabled=Off +ch25\ptp_synce\mode\probe_type=Disabled +ch26\protocol_enabled=Off +ch26\ptp_synce\mode\probe_type=Disabled +ch27\protocol_enabled=Off +ch27\ptp_synce\mode\probe_type=Disabled +ch28\protocol_enabled=Off +ch28\ptp_synce\mode\probe_type=Disabled +ch29\protocol_enabled=Off +ch29\ptp_synce\mode\probe_type=Disabled +ch30\protocol_enabled=On +ch30\ptp_synce\mode\probe_type=PTP +ch31\protocol_enabled=Off +ch31\ptp_synce\mode\probe_type=Disabled +ch32\protocol_enabled=Off +ch32\ptp_synce\mode\probe_type=Disabled +ch33\protocol_enabled=Off +ch33\ptp_synce\mode\probe_type=Disabled +ch34\protocol_enabled=Off +ch34\ptp_synce\mode\probe_type=Disabled +ch35\protocol_enabled=Off +ch35\ptp_synce\mode\probe_type=Disabled +ch36\protocol_enabled=Off +ch36\ptp_synce\mode\probe_type=Disabled +ch37\protocol_enabled=Off +ch37\ptp_synce\mode\probe_type=Disabled +ch38\protocol_enabled=Off +ch38\ptp_synce\mode\probe_type=Disabled +ch39\protocol_enabled=Off +ch39\ptp_synce\mode\probe_type=Disabled +ch40\protocol_enabled=Off +ch40\ptp_synce\mode\probe_type=Disabled +ch0\server_ip=fd00:3226:301b::1f +ch0\signal_type=1 PPS +ch0\trig_level=500 mV +ch0\freq=1 Hz +ch0\suppress_steps=Yes +ch9\ptp_synce\ntp\server_ip=fd00:3226:301b::3f +ch9\ptp_synce\ntp\server_ip_ipv6=fd00:3226:301b::3f +ch9\ptp_synce\physical_packet_channel=Channel 1 +ch9\ptp_synce\ntp\normalize_delays=Off +ch9\ptp_synce\ntp\protocol_level=UDP/IPv6 +ch9\ptp_synce\ntp\poll_log_interval=1 packet/16 s +ch30\ptp_synce\ptp\version=SPTP_V2.1 +ch30\ptp_synce\ptp\master_ip=fd00:3016:3109:face:0:1:0 +ch30\ptp_synce\ptp\master_ip_ipv6=fd00:3016:3109:face:0:1:0 +ch30\ptp_synce\physical_packet_channel=Channel 1 +ch30\ptp_synce\ptp\protocol_level=UDP/IPv6 +ch30\ptp_synce\ptp\log_announce_int=1 packet/16 s +ch30\ptp_synce\ptp\log_delay_req_int=1 packet/16 s +ch30\ptp_synce\ptp\log_sync_int=1 packet/16 s +ch30\ptp_synce\ptp\stack_mode=Unicast +ch30\ptp_synce\ptp\domain=0 +device_name=leoleovich.com +ch6\synce_enabled=Off +ch7\synce_enabled=Off +ch6\ptp_synce\ptp\dscp=0 +ch7\ptp_synce\ptp\dscp=0 +ch6\ptp_synce\ethernet\dhcp_v6=DHCP +ch7\ptp_synce\ethernet\dhcp_v6=DHCP +ch6\ptp_synce\ethernet\dhcp_v4=Disabled +ch7\ptp_synce\ethernet\dhcp_v4=Disabled +ch6\ptp_synce\ethernet\qsfp_fec=RS-FEC +ch6\virtual_channels_enabled=On +` + + var c config + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, + r *http.Request) { + fmt.Fprintln(w, testConfig) + })) + defer ts.Close() + + parsed, _ := url.Parse(ts.URL) + calnexAPI := api.NewAPI(parsed.Host, true) + calnexAPI.Client = ts.Client() + cc := &CalnexConfig{ + AntennaDelayNS: 42, + Measure: map[api.Channel]MeasureConfig{ + api.ChannelA: { + Target: "fd00:3226:301b::1f", + Probe: api.ProbePPS, + }, + api.ChannelVP1: { + Target: "fd00:3226:301b::3f", + Probe: api.ProbeNTP, + }, + api.ChannelVP22: { + Target: "fd00:3016:3109:face:0:1:0", + Probe: api.ProbePTP, + }, + }, + } + + f, err := prepare(&c, calnexAPI, "leoleovich.com", cc) + require.NoError(t, err) + require.True(t, c.changed) + + buf, err := api.ToBuffer(f) + require.NoError(t, err) + require.Equal(t, expectedConfig, buf.String()) +}