Olares/daemon/pkg/commands/change_ip/cmd.go
Peng Peng 61d793e4c6
cli, daemon: add olares-cli status backed by olaresd /system/status (#2917)
* cli, daemon: add `olares-cli status` backed by olaresd /system/status

Adds a new top-level `olares-cli status` command that calls the local
olaresd daemon's `/system/status` HTTP endpoint and prints either a
grouped, annotated human-readable report or raw JSON.

To avoid duplicating the response schema, the daemon's `state` struct
and related enums are extracted into a new shared package
`cli/pkg/daemon/state`. The daemon now type-aliases those types so all
existing daemon call sites keep compiling unchanged.

Made-with: Cursor

* cli: drop unused state.APIResponse envelope type

The HTTP client in cli/pkg/daemon/api/client.go intentionally uses an
inline anonymous envelope with json.RawMessage for the data field so
that --json mode can passthrough the bytes verbatim, so the public
APIResponse{Data State} type defined here had zero references. Remove
it; if a strongly-typed consumer ever shows up, re-add then.

Addresses Cursor Bugbot feedback on PR #2917.

Made-with: Cursor
2026-04-21 00:29:50 +08:00

189 lines
4.5 KiB
Go

package changeip
import (
"context"
"io"
"os"
"strings"
"time"
"github.com/beclab/Olares/daemon/pkg/cli"
"github.com/beclab/Olares/daemon/pkg/cluster/state"
"github.com/beclab/Olares/daemon/pkg/commands"
"github.com/beclab/Olares/daemon/pkg/utils"
"github.com/nxadm/tail"
"k8s.io/klog/v2"
)
type changeIp struct {
commands.Operation
isRetryChange bool
notInstalled bool
logFile string
}
var _ commands.Interface = &changeIp{}
func New() commands.Interface {
return &changeIp{
Operation: commands.Operation{
Name: commands.ChangeIp,
},
logFile: commands.COMMAND_BASE_DIR + "/logs/changeip.log",
}
}
func (i *changeIp) Execute(ctx context.Context, p any) (res any, err error) {
i.isRetryChange = state.CurrentState.TerminusState == state.IPChangeFailed
cmd := commands.NewBaseCommand()
cmd.WithDir_(commands.COMMAND_BASE_DIR).
WithPid_(commands.CHANGINGIP_PID_FILE).
AddEnv_("QUIET", "1").
WithWatchDog_(i.watch)
if _, err := os.Stat(commands.INSTALL_LOCK); err != nil && os.IsNotExist(err) {
cmd.AddEnv_("NOT_INSTALLED", "1")
i.notInstalled = true
}
if _, err := os.Stat(commands.PREPARE_LOCK); err != nil && os.IsNotExist(err) {
cmd.AddEnv_("NOT_PREPARED", "1")
}
if _, err := os.Stat(i.logFile); err == nil {
if err = os.Remove(i.logFile); err != nil {
klog.Error("remove prev log file error, ", err)
}
}
var cmds []string = []string{
"change-ip",
"--version", commands.INSTALLED_VERSION,
"--base-dir", commands.TERMINUS_BASE_DIR,
}
// FIXME: maybe we can always get master node ip from etcd config or anything else.
masterIp, err := utils.MasterNodeIp(!i.notInstalled)
if err != nil {
klog.Error("get master node ip error,", err)
}
// backup prev ip, if change failed we can resume with this ip
err = os.WriteFile(commands.PREV_IP_TO_CHANGE_FILE, []byte(masterIp), 0644)
if err != nil {
klog.Error("cannot backup prev ip, ", err)
}
state.ChangeTerminusStateTo(state.IPChanging)
// remove PREV_IP_CHANGE_FAILED tag file if exists
if _, err = os.Stat(commands.PREV_IP_CHANGE_FAILED); err == nil {
if err = os.Remove(commands.PREV_IP_CHANGE_FAILED); err != nil {
klog.Warning("remove ip change failed tag file error, ", err)
}
}
if err = cmd.RunAsync_(ctx, cli.TERMINUS_CLI, cmds...); err != nil {
i.setFailedState()
return nil, err
}
return nil, nil
}
func (i *changeIp) watch(ctx context.Context) {
go func() {
// delay starting to watch
time.Sleep(10 * time.Second)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
successState := state.TerminusRunning
if i.notInstalled {
successState = state.NotInstalled
}
for {
select {
case <-ctx.Done():
klog.Info("change ip command finished")
_, err := os.Stat(commands.CHANGINGIP_PID_FILE)
if os.IsNotExist(err) {
// double check
if i.tailLog() {
klog.Info("change ip command finished, change state")
state.ChangeTerminusStateTo(successState)
return
} else {
klog.Warning("check log file, process not succeed")
i.setFailedState()
return
}
}
klog.Warning("change ip killed")
state.ChangeTerminusStateTo(state.SystemError)
return
case <-ticker.C:
if i.tailLog() {
klog.Info("watch log succeed")
state.ChangeTerminusStateTo(successState)
return
}
}
}
}()
}
func (i *changeIp) tailLog() (finished bool) {
tailFile := i.logFile
info, err := os.Stat(tailFile)
if err != nil {
klog.Error(err)
return false
}
filesize := info.Size()
tailsize := min(filesize, 512)
t, err := tail.TailFile(tailFile,
tail.Config{Follow: false, Location: &tail.SeekInfo{Offset: -tailsize, Whence: io.SeekEnd}})
defer t.Stop()
if err != nil {
klog.Error("tail log error, ", err)
return false
}
keyWords := "Olares OS components execute successfully!"
var line *tail.Line
for line = range t.Lines {
if strings.Contains(line.Text, keyWords) {
return true
}
}
if line != nil {
// for debug
klog.Info(line.Text)
}
return false
}
func (i *changeIp) setFailedState() {
if i.isRetryChange {
klog.Error("retry ip change failed")
state.ChangeTerminusStateTo(state.SystemError)
} else {
// create a ip change failed tag file, make the ip-change command can
// be resumed if the device get reboot
err := os.WriteFile(commands.PREV_IP_CHANGE_FAILED, []byte(time.Now().String()), 0644)
if err != nil {
klog.Error("write ip change failed tag file error, ", err)
}
state.ChangeTerminusStateTo(state.IPChangeFailed)
}
}