calnex/api/api.go (621 lines of code) (raw):
/*
Copyright (c) Facebook, Inc. and its affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"bytes"
"crypto/tls"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/go-ini/ini"
)
// API is struct for accessing calnex API
type API struct {
Client *http.Client
source string
}
// Status is a struct representing Calnex status JSON response
type Status struct {
ReferenceReady bool
ModulesReady bool
MeasurementActive bool
}
// Result is a struct representing Calnex result JSON response
type Result struct {
Result bool
Message string
}
// Version is a struct representing Calnex version JSON response
type Version struct {
Firmware string
}
// GNSS is a struct representing Calnex GNSS JSON response
type GNSS struct {
AntennaStatus string
Locked bool
LockedSatellites int
SurveyComplete bool
SurveyPercentComplete int
}
// Channel is a Calnex channel object
type Channel int
// Calnex Status contants
const (
ON = "On"
OFF = "Off"
YES = "Yes"
NO = "No"
ENABLED = "Enabled"
DISABLED = "Disabled"
STATIC = "Static"
DHCP = "DHCP"
TE = "te"
TWOWAYTE = "2wayte"
)
// Available Calnex channels
const (
ChannelA Channel = iota
ChannelB
ChannelC
ChannelD
ChannelE
ChannelF
ChannelONE
ChannelTWO
ChannelREF
ChannelVP1
ChannelVP2
ChannelVP3
ChannelVP4
ChannelVP5
ChannelVP6
ChannelVP7
ChannelVP8
ChannelVP9
ChannelVP10
ChannelVP11
ChannelVP12
ChannelVP13
ChannelVP14
ChannelVP15
ChannelVP16
ChannelVP17
ChannelVP18
ChannelVP19
ChannelVP20
ChannelVP21
ChannelVP22
ChannelVP23
ChannelVP24
ChannelVP25
ChannelVP26
ChannelVP27
ChannelVP28
ChannelVP29
ChannelVP30
ChannelVP31
ChannelVP32
)
// MeasureChannelDatatypeMap is a Map of the measurement channels to the data type.
// Only channels used for measurements defined here
var MeasureChannelDatatypeMap = map[Channel]string{
ChannelA: TE,
ChannelB: TE,
ChannelC: TE,
ChannelD: TE,
ChannelE: TE,
ChannelF: TE,
ChannelVP1: TWOWAYTE,
ChannelVP2: TWOWAYTE,
ChannelVP3: TWOWAYTE,
ChannelVP4: TWOWAYTE,
ChannelVP5: TWOWAYTE,
ChannelVP6: TWOWAYTE,
ChannelVP7: TWOWAYTE,
ChannelVP8: TWOWAYTE,
ChannelVP9: TWOWAYTE,
ChannelVP10: TWOWAYTE,
ChannelVP11: TWOWAYTE,
ChannelVP12: TWOWAYTE,
ChannelVP13: TWOWAYTE,
ChannelVP14: TWOWAYTE,
ChannelVP15: TWOWAYTE,
ChannelVP16: TWOWAYTE,
ChannelVP17: TWOWAYTE,
ChannelVP18: TWOWAYTE,
ChannelVP19: TWOWAYTE,
ChannelVP20: TWOWAYTE,
ChannelVP21: TWOWAYTE,
ChannelVP22: TWOWAYTE,
ChannelVP23: TWOWAYTE,
ChannelVP24: TWOWAYTE,
ChannelVP25: TWOWAYTE,
ChannelVP26: TWOWAYTE,
ChannelVP27: TWOWAYTE,
ChannelVP28: TWOWAYTE,
ChannelVP29: TWOWAYTE,
ChannelVP30: TWOWAYTE,
ChannelVP31: TWOWAYTE,
ChannelVP32: TWOWAYTE,
}
// channelStringToCalnex is a map of String channels to a Calnex variant
var channelStringToCalnex = map[string]Channel{
"a": ChannelA,
"b": ChannelB,
"c": ChannelC,
"d": ChannelD,
"e": ChannelE,
"f": ChannelF,
"1": ChannelONE,
"2": ChannelTWO,
"VP1": ChannelVP1,
"VP2": ChannelVP2,
"VP3": ChannelVP3,
"VP4": ChannelVP4,
"VP5": ChannelVP5,
"VP6": ChannelVP6,
"VP7": ChannelVP7,
"VP8": ChannelVP8,
"VP9": ChannelVP9,
"VP10": ChannelVP10,
"VP11": ChannelVP11,
"VP12": ChannelVP12,
"VP13": ChannelVP13,
"VP14": ChannelVP14,
"VP15": ChannelVP15,
"VP16": ChannelVP16,
"VP17": ChannelVP17,
"VP18": ChannelVP18,
"VP19": ChannelVP19,
"VP20": ChannelVP20,
"VP21": ChannelVP21,
"VP22": ChannelVP22,
"VP23": ChannelVP23,
"VP24": ChannelVP24,
"VP25": ChannelVP25,
"VP26": ChannelVP26,
"VP27": ChannelVP27,
"VP28": ChannelVP28,
"VP29": ChannelVP29,
"VP30": ChannelVP30,
"VP31": ChannelVP31,
"VP32": ChannelVP32,
}
// channelCalnexToString is a map of Calnex channels to a String variant
var channelCalnexToString = map[Channel]string{
ChannelA: "a",
ChannelB: "b",
ChannelC: "c",
ChannelD: "d",
ChannelE: "e",
ChannelF: "f",
ChannelONE: "1",
ChannelTWO: "2",
ChannelVP1: "VP1",
ChannelVP2: "VP2",
ChannelVP3: "VP3",
ChannelVP4: "VP4",
ChannelVP5: "VP5",
ChannelVP6: "VP6",
ChannelVP7: "VP7",
ChannelVP8: "VP8",
ChannelVP9: "VP9",
ChannelVP10: "VP10",
ChannelVP11: "VP11",
ChannelVP12: "VP12",
ChannelVP13: "VP13",
ChannelVP14: "VP14",
ChannelVP15: "VP15",
ChannelVP16: "VP16",
ChannelVP17: "VP17",
ChannelVP18: "VP18",
ChannelVP19: "VP19",
ChannelVP20: "VP20",
ChannelVP21: "VP21",
ChannelVP22: "VP22",
ChannelVP23: "VP23",
ChannelVP24: "VP24",
ChannelVP25: "VP25",
ChannelVP26: "VP26",
ChannelVP27: "VP27",
ChannelVP28: "VP28",
ChannelVP29: "VP29",
ChannelVP30: "VP30",
ChannelVP31: "VP31",
ChannelVP32: "VP32",
}
// ChannelFromString returns Channel object from String version
func ChannelFromString(value string) (*Channel, error) {
c, ok := channelStringToCalnex[value]
if !ok {
return nil, errBadChannel
}
return &c, nil
}
// String returns String friendly channel name like "a" or "2"
func (c Channel) String() string {
return channelCalnexToString[c]
}
// UnmarshalText channel from string version
func (c *Channel) UnmarshalText(value []byte) error {
cr, err := ChannelFromString(string(value))
if err != nil {
return err
}
*c = *cr
return nil
}
// Calnex returns calnex friendly channel name like 1 or 7
func (c Channel) Calnex() int {
return int(c)
}
// CalnexAPI returns channel name in API format like "ch2"
func (c Channel) CalnexAPI() string {
return fmt.Sprintf("ch%d", c.Calnex())
}
// Probe is a Calnex probe protocol
type Probe int
// Probe numbers by calnex
const (
ProbePTP Probe = 0
ProbeNTP Probe = 2
ProbePPS Probe = 3
)
// probeStringToProbe is a map of String probe to a Calnex variant
var probeStringToProbe = map[string]Probe{
"ptp": ProbePTP,
"ntp": ProbeNTP,
"pps": ProbePPS,
}
// probeCalnexToProbe is a map of Calnex to a probe variant
var probeCalnexAPIToProbe = map[string]Probe{
fmt.Sprintf("%d", int(ProbePTP)): ProbePTP,
fmt.Sprintf("%d", int(ProbeNTP)): ProbeNTP,
"1 PPS": ProbePPS,
}
// probeToString is a map of probe to String variant
var probeToString = map[Probe]string{
ProbePTP: "ptp",
ProbeNTP: "ntp",
ProbePPS: "pps",
}
// probeToCalnexName is a map of probe to a Calnex specific name
var probeToCalnexName = map[Probe]string{
ProbePTP: "PTP",
ProbeNTP: "NTP",
ProbePPS: "1 PPS",
}
// probeToServerType is a map of probe to Calnex server name
var probeToServerType = map[Probe]string{
ProbePTP: "master_ip",
ProbeNTP: "server_ip",
ProbePPS: "server_ip",
}
// ProbeFromString returns Channel object from String version
func ProbeFromString(value string) (*Probe, error) {
p, ok := probeStringToProbe[value]
if !ok {
return nil, errBadProbe
}
return &p, nil
}
// ProbeFromCalnex returns Channel object from String version
func ProbeFromCalnex(calnex string) (*Probe, error) {
p, ok := probeCalnexAPIToProbe[calnex]
if !ok {
return nil, errBadProbe
}
return &p, nil
}
// String returns String friendly probe name like "ntp" or "ptp"
func (p Probe) String() string {
return probeToString[p]
}
// UnmarshalText probe from string version
func (p *Probe) UnmarshalText(value []byte) error {
pr, err := ProbeFromString(string(value))
if err != nil {
return err
}
*p = *pr
return nil
}
// ServerType returns server type like "server_ip" or "master_ip"
func (p Probe) ServerType() string {
return probeToServerType[p]
}
// CalnexName returns Calnex Name like "PTP slave" or "NTP client"
func (p Probe) CalnexName() string {
return probeToCalnexName[p]
}
const (
// measureURL is a base URL for to the measurement API
measureURL = "https://%s/api/get/measure/%s"
dataURL = "https://%s/api/getdata?channel=%s&datatype=%s&reset=true"
startMeasure = "https://%s/api/startmeasurement"
stopMeasure = "https://%s/api/stopmeasurement"
getSettingsURL = "https://%s/api/getsettings"
setSettingsURL = "https://%s/api/setsettings"
getStatusURL = "https://%s/api/getstatus"
getProblemReportURL = "https://%s/api/getproblemreport"
clearDeviceURL = "https://%s/api/cleardevice?action=cleardevice"
rebootURL = "https://%s/api/reboot?action=reboot"
versionURL = "https://%s/api/version"
firmwareURL = "https://%s/api/updatefirmware"
certificateURL = "https://%s/api/installcertificate"
gnssURL = "https://%s/api/gnss/status"
)
var (
errBadChannel = errors.New("channel is not recognized")
errBadProbe = errors.New("probe protocol is not recognized")
errAPI = errors.New("invalid response from API")
)
func parseResponse(response string) (string, error) {
s := strings.Split(strings.TrimSuffix(response, "\n"), "=")
if len(s) != 2 {
return "", errAPI
}
return s[1], nil
}
// NewAPI returns an pointer of API struct with default values.
func NewAPI(source string, insecureTLS bool) *API {
return &API{
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureTLS},
},
Timeout: 2 * time.Minute,
},
source: source,
}
}
// FetchCsv takes channel name (like 1, 2, c, d)
// it returns list of CSV lines which is []string
func (a *API) FetchCsv(channel Channel) ([][]string, error) {
url := fmt.Sprintf(dataURL, a.source, channel, MeasureChannelDatatypeMap[channel])
resp, err := a.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Check for empty response
r := &Result{}
if err = json.Unmarshal(b, r); err == nil {
return nil, fmt.Errorf(r.Message)
}
var res [][]string
csvReader := csv.NewReader(bytes.NewReader(b))
csvReader.Comment = '#'
for {
csvLine, err := csvReader.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return nil, fmt.Errorf("failed to parse csv for data from channel %s: %v", channel.String(), err)
}
}
res = append(res, csvLine)
}
return res, nil
}
// FetchChannelProbe returns monitored protocol of the channel
func (a *API) FetchChannelProbe(channel Channel) (*Probe, error) {
pth := path.Join(channel.CalnexAPI(), "ptp_synce", "mode", "probe_type")
if MeasureChannelDatatypeMap[channel] == TE {
pth = path.Join(channel.CalnexAPI(), "signal_type")
}
url := fmt.Sprintf(measureURL, a.source, pth)
resp, err := a.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
probe, err := parseResponse(string(b))
if err != nil {
return nil, err
}
p, err := ProbeFromCalnex(probe)
return p, err
}
// FetchChannelTarget returns the measure target of the server monitored on the channel
func (a *API) FetchChannelTarget(channel Channel, probe Probe) (string, error) {
pth := path.Join(channel.CalnexAPI(), "ptp_synce", probe.String(), probe.ServerType())
if MeasureChannelDatatypeMap[channel] == TE {
pth = path.Join(channel.CalnexAPI(), probe.ServerType())
}
url := fmt.Sprintf(measureURL, a.source, pth)
resp, err := a.Client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New(http.StatusText(resp.StatusCode))
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return parseResponse(string(b))
}
// FetchUsedChannels returns list of channels in use
func (a *API) FetchUsedChannels() ([]Channel, error) {
channels := []Channel{}
f, err := a.FetchSettings()
if err != nil {
return channels, err
}
for ch := range MeasureChannelDatatypeMap {
chInstalled := f.Section("measure").Key(fmt.Sprintf("%s\\installed", ch.CalnexAPI())).String()
if chInstalled != "1" {
continue
}
chStatus := f.Section("measure").Key(fmt.Sprintf("%s\\used", ch.CalnexAPI())).String()
if chStatus == "Yes" {
channels = append(channels, ch)
}
}
return channels, err
}
// FetchSettings returns the calnex settings
func (a *API) FetchSettings() (*ini.File, error) {
url := fmt.Sprintf(getSettingsURL, a.source)
resp, err := a.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
return ini.Load(resp.Body)
}
// FetchStatus returns the calnex status
func (a *API) FetchStatus() (*Status, error) {
url := fmt.Sprintf(getStatusURL, a.source)
resp, err := a.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
s := &Status{}
if err = json.NewDecoder(resp.Body).Decode(s); err != nil {
return nil, err
}
return s, nil
}
// FetchProblemReport saves a problem report
func (a *API) FetchProblemReport(dir string) (string, error) {
url := fmt.Sprintf(getProblemReportURL, a.source)
resp, err := a.Client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New(http.StatusText(resp.StatusCode))
}
// calnex_problem_report_2021-12-07_10-42-26.tar
reportFileName := path.Join(dir, fmt.Sprintf("calnex_problem_report_%s.tar", time.Now().Format("2006-01-02_15-04-05")))
reportF, err := os.Create(reportFileName)
if err != nil {
return "", err
}
defer reportF.Close()
_, err = io.Copy(reportF, resp.Body)
if err != nil {
return "", err
}
return reportFileName, nil
}
// FetchVersion returns current Firmware Version
func (a *API) FetchVersion() (*Version, error) {
url := fmt.Sprintf(versionURL, a.source)
resp, err := a.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
v := &Version{}
if err = json.NewDecoder(resp.Body).Decode(v); err != nil {
return nil, err
}
return v, nil
}
// PushVersion uploads a new Firmware Version to the device
func (a *API) PushVersion(path string) (*Result, error) {
fw, err := os.Open(path)
if err != nil {
return nil, err
}
defer fw.Close()
url := fmt.Sprintf(firmwareURL, a.source)
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(fw)
if err != nil {
return nil, err
}
r, err := a.post(url, buf)
return r, err
}
// PushCert uploads a new Certificate to the device
func (a *API) PushCert(cert []byte) (*Result, error) {
url := fmt.Sprintf(certificateURL, a.source)
buf := bytes.NewBuffer(cert)
r, err := a.post(url, buf)
return r, err
}
// PushSettings pushes the calnex settings
func (a *API) PushSettings(f *ini.File) error {
buf, err := ToBuffer(f)
if err != nil {
return err
}
url := fmt.Sprintf(setSettingsURL, a.source)
_, err = a.post(url, buf)
return err
}
func (a *API) post(url string, content *bytes.Buffer) (*Result, error) {
// content must be a bytes.Buffer or anything which supports .Len()
// Otherwise Content-Length will not be set.
resp, err := a.Client.Post(url, "application/x-www-form-urlencoded", content)
if err != nil {
return nil, err
}
defer resp.Body.Close()
r := &Result{}
if err = json.NewDecoder(resp.Body).Decode(r); err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return r, errors.New(http.StatusText(resp.StatusCode))
}
if !r.Result {
return nil, errors.New(r.Message)
}
return r, nil
}
func (a *API) get(path string) error {
url := fmt.Sprintf(path, a.source)
resp, err := a.Client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(http.StatusText(resp.StatusCode))
}
r := &Result{}
if err = json.NewDecoder(resp.Body).Decode(r); err != nil {
return err
}
if !r.Result {
return errors.New(r.Message)
}
return nil
}
// StartMeasure starts measurement
func (a *API) StartMeasure() error {
return a.get(startMeasure)
}
// StopMeasure stops measurement
func (a *API) StopMeasure() error {
return a.get(stopMeasure)
}
// ClearDevice clears device data
func (a *API) ClearDevice() error {
// check measurement status
status, err := a.FetchStatus()
if err != nil {
return err
}
if status.MeasurementActive {
// stop measurement
if err = a.StopMeasure(); err != nil {
return err
}
}
return a.get(clearDeviceURL)
}
// Reboot the device
func (a *API) Reboot() error {
// check measurement status
status, err := a.FetchStatus()
if err != nil {
return err
}
if status.MeasurementActive {
// stop measurement
if err = a.StopMeasure(); err != nil {
return err
}
}
return a.get(rebootURL)
}
// GnssStatus returns current GNSS status
func (a *API) GnssStatus() (*GNSS, error) {
url := fmt.Sprintf(gnssURL, a.source)
resp, err := a.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(http.StatusText(resp.StatusCode))
}
g := &GNSS{}
if err = json.NewDecoder(resp.Body).Decode(g); err != nil {
return nil, err
}
return g, nil
}