package rpc import ( "context" "fmt" "log" "net/http" "time" "forge.cadoles.com/Pyxis/orion/emlid" "forge.cadoles.com/Pyxis/orion/emlid/updater" "forge.cadoles.com/Pyxis/orion/openwrt" "github.com/gorilla/rpc" "github.com/gorilla/rpc/json" "github.com/pkg/errors" ) // OrionService is the JSON-RPC API type OrionService struct { UCI *openwrt.UCI } // NewOrionService create a new OrionService ! func NewOrionService() *OrionService { uci := openwrt.NewUCI() return &OrionService{ UCI: uci, } } // UCIArgs argument structure for exported method OwrtListWifiDevices type UCIArgs struct{} // UCIResponse is the response structure for exposed method OwrtListWifiDevices type UCIResponse struct { Devices []map[string]string } // OwrtListWifiDevices offers an RPC Method to list Wifi nics in a OpenWRT device. func (o *OrionService) OwrtListWifiDevices(r *http.Request, args *UCIArgs, reply *UCIResponse) error { o.UCI.LoadWirelessConf() devs := o.UCI.GetWifiDevices() reply.Devices = devs return nil } // ListIfaceArgs argument structure for exported method OwrtListWifiDevices type ListIfaceArgs struct{} // ListIfaceResponse is the response structure for exposed method OwrtListWifiDevices type ListIfaceResponse struct { Interfaces []*openwrt.UCIWirelessInterface } // OwrtListWifiInterfaces offers an RPC Method to list wifi interfaces in a OpenWRT device. func (o *OrionService) OwrtListWifiInterfaces(r *http.Request, args *ListIfaceArgs, reply *ListIfaceResponse) error { o.UCI.LoadWirelessConf() reply.Interfaces = o.UCI.GetWifiIfaces() return nil } // CreateIfaceArgs argument structure for exported method OwrtCreateWifiInterface type CreateIfaceArgs struct { Iface *openwrt.UCIWirelessInterface Cleanup bool } // CreateIfaceResponse argument structure for exported method OwrtCreateWifiInterface type CreateIfaceResponse struct { Iface *openwrt.UCIWirelessInterface Errors []*openwrt.Action } // OwrtCreateWifiInterface Create a WifiInterface in openwrt func (o *OrionService) OwrtCreateWifiInterface(r *http.Request, args *CreateIfaceArgs, reply *CreateIfaceResponse) error { reply.Iface = nil if args.Cleanup { o.UCI.LoadWirelessConf() w := o.UCI.GetWifiIfaces() for _, iface := range w { iface.Delete(o.UCI) } o.UCI.Commit() o.UCI.Reload() time.Sleep(3 * time.Second) } o.UCI.LoadWirelessConf() create := args.Iface.Create(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } create = args.Iface.SysAdd(o.UCI) if create.ReturnCode == 233 { delete := args.Iface.SysDel(o.UCI) if delete.ReturnCode != 0 { reply.Errors = append(reply.Errors, delete) return nil } create = args.Iface.SysAdd(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } } create = args.Iface.Up(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } create = args.Iface.Save(o.UCI) if create.ReturnCode != 0 { reply.Errors = append(reply.Errors, create) return nil } reply.Iface = args.Iface return nil } // ConnectIfaceArgs argument structure for exported method OwrtCreateWifiInterface type ConnectIfaceArgs struct { Iface *openwrt.UCIWirelessInterface SSID string Key string } // ConnectIfaceResponse argument structure for exported method OwrtCreateWifiInterface type ConnectIfaceResponse struct { status int Errors []string } // OwrtConnectWifiInterface connects a given Wifi Interface to a given SSID func (o *OrionService) OwrtConnectWifiInterface(r *http.Request, args *ConnectIfaceArgs, reply *ConnectIfaceResponse) error { o.UCI.LoadWirelessConf() iface := o.UCI.GetWifiIface(args.Iface.Index) cells := iface.Scan() for _, cell := range cells { if cell.Ssid == args.SSID { cn := iface.Connect(o.UCI, cell, args.Key) if cn.ReturnCode != 0 { reply.status = cn.ReturnCode reply.Errors = append(reply.Errors, cn.Stdout, cn.Stderr, cn.Command) } reply.status = 0 return nil } } reply.status = 1 msg := fmt.Sprintf("Wifi Cell with SSID %s is not available !", args.SSID) reply.Errors = append(reply.Errors, msg) return nil } // OrionBox describe an fresh Orion box (base or rover) type OrionBox struct { Address string NewAddress string SSID string Security string WifiKey string } // OrionServer describe the Orion master server type OrionServer struct { Address string SSID string Security string WifiKey string ClientIface *openwrt.UCIWirelessInterface } // UpdateOrionBoxArgs argument structure for exported method OwrtCreateWifiInterface type UpdateOrionBoxArgs struct { Box *OrionBox Server *OrionServer } // UpdateOrionBoxResponse argument structure for exported method OwrtCreateWifiInterface type UpdateOrionBoxResponse struct { IP string Netmask string Synced bool Version string Status *updater.UpdateStatus Results *updater.TestResults } // Connect wifi interface to a Orion box wifi hotspot! func (o *OrionService) connectBox(box *OrionBox, server *OrionServer) error { o.UCI.LoadWirelessConf() if server == nil { return fmt.Errorf("Server definition is empty") } if box == nil { return fmt.Errorf("Box definitioni is emtpy") } iface := server.ClientIface cells := iface.Scan() for _, cell := range cells { if cell.Ssid == box.SSID { cn := iface.Connect(o.UCI, cell, box.WifiKey) if cn.ReturnCode != 0 { return fmt.Errorf("%s\n%s", cn.Stdout, cn.Stderr) } dhcli := openwrt.NewDhcpClient(iface.SysDevName) dhres := dhcli.AskForIP() if dhres.CmdRes.ReturnCode != 0 { return fmt.Errorf("%s\n%s", cn.Stdout, cn.Stderr) } re := o.UCI.Reload() if re.ReturnCode != 0 { return fmt.Errorf("%s\n%s", re.Stdout, re.Stderr) } return nil } } return fmt.Errorf("Wifi cell with SSID %s is not available", box.SSID) } func (o *OrionService) discoverService() ([]emlid.Service, error) { return emlid.Discover(20 * time.Second) } // setupMasterWifi connects to the box, add wifi network and join it ! func (o *OrionService) setupMasterWifi(rqContext context.Context, box *OrionBox, server *OrionServer) error { if err := o.connectBox(box, server); err != nil { return errors.Wrap(err, "Connect to box failed") } if server.Security == "" { server.Security = string(updater.SecurityWPAPSK) } var boxCli *updater.Client service, err := o.discoverService() if len(service) == 0 { boxCli = updater.NewClient( emlid.WithEndpoint(box.Address, 80), ) } else if err != nil { boxCli = updater.NewClient( emlid.WithService(service[0]), ) } else { return err } if err := boxCli.Connect(); err != nil { return errors.Wrap(err, "Connecting to Box failed") } defer boxCli.Close() ctx, testResultsCancel := context.WithTimeout(rqContext, 55*time.Second) defer testResultsCancel() _, err = boxCli.TestResults(ctx) if err != nil { return errors.Wrap(err, "Minimal test failed") } ctx, addWifiCancel := context.WithTimeout(rqContext, 55*time.Second) defer addWifiCancel() done, err := boxCli.AddWifiNetwork(ctx, server.SSID, updater.WifiSecurity(server.Security), server.WifiKey) if err != nil { return errors.Wrap(err, "AddWifiNetworkFailed") } if !done { return fmt.Errorf("Impossible to add wifi network") } ctx, joinWifiCancel := context.WithTimeout(ctx, 55*time.Second) defer joinWifiCancel() if err := boxCli.JoinWifiNetwork(ctx, server.SSID, true); err != nil { return errors.Wrap(err, "Time sync failed") } return nil } // updateAndReboot connects to the box with the New adress and run basic tests on version dans updates func (o *OrionService) updateAndReboot(rqContext context.Context, box *OrionBox, server *OrionServer, reply *UpdateOrionBoxResponse) error { var boxCli *updater.Client service, err := o.discoverService() if err != nil { boxCli = updater.NewClient( emlid.WithService(service[0]), ) } else { boxCli = updater.NewClient( emlid.WithEndpoint(box.NewAddress, 80), ) } if err := boxCli.Connect(); err != nil { if err := boxCli.Connect(); err != nil { return errors.Wrap(err, "Connecting to Box on master wifi network failed") } } defer boxCli.Close() ctx, timeSyncedCancel := context.WithTimeout(rqContext, 55*time.Second) defer timeSyncedCancel() synced, err := boxCli.TimeSynced(ctx) if err != nil { return errors.Wrap(err, "Time sync failed") } reply.Synced = synced ctx, reachviewVersionCancel := context.WithTimeout(rqContext, 55*time.Second) defer reachviewVersionCancel() version, err := boxCli.ReachViewVersion(ctx) if err != nil { return err } reply.Version = version ctx, updateCancel := context.WithTimeout(rqContext, 55*time.Second) defer updateCancel() status, err := boxCli.Update(ctx) if err != nil { return errors.Wrap(err, "System update failed") } reply.Status = status if err := boxCli.SkipUpdate(); err != nil { log.Fatalf("error while skipping update: %s", err) } ctx, rebootCancel := context.WithTimeout(rqContext, 55*time.Second) defer rebootCancel() return boxCli.RebootNow(ctx, true) } // UpdateOrionBox starts provisionning process for an Orion box (base or rover) func (o *OrionService) UpdateOrionBox(r *http.Request, args *UpdateOrionBoxArgs, reply *UpdateOrionBoxResponse) error { if err := o.setupMasterWifi(r.Context(), args.Box, args.Server); err != nil { return err } return o.updateAndReboot(r.Context(), args.Box, args.Server, reply) } // NewServer returns a new configured JSON-RPC server func NewServer() *rpc.Server { server := rpc.NewServer() server.RegisterCodec(json.NewCodec(), "application/json") if err := server.RegisterService(NewOrionService(), "Orion"); err != nil { panic(err) } return server }