diff --git a/cmd/climc/shell/compute/servers.go b/cmd/climc/shell/compute/servers.go index a3e8d193db..66df257e91 100644 --- a/cmd/climc/shell/compute/servers.go +++ b/cmd/climc/shell/compute/servers.go @@ -115,6 +115,8 @@ func init() { cmd.Perform("qga-set-password", &options.ServerQgaSetPassword{}) cmd.Perform("qga-command", &options.ServerQgaCommand{}) cmd.Perform("qga-ping", &options.ServerQgaPing{}) + cmd.Perform("qga-guest-info-task", &options.ServerQgaGuestInfoTask{}) + cmd.Perform("qga-get-network", &options.ServerQgaGetNetwork{}) cmd.Perform("set-password", &options.ServerSetPasswordOptions{}) cmd.Perform("set-boot-index", &options.ServerSetBootIndexOptions{}) cmd.Perform("reset-nic-traffic-limit", &options.ServerNicTrafficLimitOptions{}) diff --git a/pkg/apis/compute/guest_const.go b/pkg/apis/compute/guest_const.go index 21a3db3eb0..edd84595e2 100644 --- a/pkg/apis/compute/guest_const.go +++ b/pkg/apis/compute/guest_const.go @@ -167,6 +167,9 @@ const ( VM_QGA_COMMAND_EXECUTING = "qga_command_executing" VM_QGA_EXEC_COMMAND_FAILED = "qga_exec_command_failed" + VM_QGA_SET_NETWORK = "qga_set_network" + VM_QGA_SET_NETWORK_FAILED = "qga_set_network_failed" + SHUTDOWN_STOP = "stop" SHUTDOWN_TERMINATE = "terminate" diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index cc732f3519..2c366dacd6 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -985,11 +985,46 @@ type ServerQemuInfo struct { Cmdline string `json:"cmdline"` } +type IPAddress struct { + IPAddress string `json:"ip-address"` + IPAddressType string `json:"ip-address-type"` + Prefix int `json:"prefix"` +} + +type IfnameDetail struct { + HardwareAddress string `json:"hardware-address"` + IPAddresses []IPAddress `json:"ip-addresses"` + Name string `json:"name"` + Statistics struct { + RxBytes int `json:"rx-bytes"` + RxDropped int `json:"rx-dropped"` + RxErrs int `json:"rx-errs"` + RxPackets int `json:"rx-packets"` + TxBytes int `json:"tx-bytes"` + TxDropped int `json:"tx-dropped"` + TxErrs int `json:"tx-errs"` + TxPackets int `json:"tx-packets"` + } `json:"statistics"` +} + type ServerQgaSetPasswordInput struct { Username string Password string } +type ServerQgaGuestInfoTaskInput struct { +} + +type ServerQgaSetNetworkInput struct { + ServerQgaTimeoutInput + Device string + Ipmask string + Gateway string +} + +type ServerQgaGetNetworkInput struct { +} + type ServerQgaTimeoutInput struct { // qga execute timeout millisecond Timeout int diff --git a/pkg/compute/guestdrivers/base.go b/pkg/compute/guestdrivers/base.go index b4d1bbdc1b..04eafdf2c9 100644 --- a/pkg/compute/guestdrivers/base.go +++ b/pkg/compute/guestdrivers/base.go @@ -504,6 +504,18 @@ func (drv *SBaseGuestDriver) QgaRequestSetUserPassword(ctx context.Context, task return httperrors.ErrNotImplemented } +func (self *SBaseGuestDriver) QgaRequestGuestInfoTask(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { + return nil, httperrors.ErrNotImplemented +} + +func (self *SBaseGuestDriver) QgaRequestSetNetwork(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { + return nil, httperrors.ErrNotImplemented +} + +func (self *SBaseGuestDriver) QgaRequestGetNetwork(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { + return nil, httperrors.ErrNotImplemented +} + func (drv *SBaseGuestDriver) RequestQgaCommand(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { return nil, httperrors.ErrNotImplemented } diff --git a/pkg/compute/guestdrivers/kvm.go b/pkg/compute/guestdrivers/kvm.go index 3ff2d72127..2a5883ba22 100644 --- a/pkg/compute/guestdrivers/kvm.go +++ b/pkg/compute/guestdrivers/kvm.go @@ -1059,6 +1059,39 @@ func (self *SKVMGuestDriver) QgaRequestGuestPing(ctx context.Context, header htt return nil } +func (self *SKVMGuestDriver) QgaRequestGuestInfoTask(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/servers/%s/qga-guest-info-task", host.ManagerUri, guest.Id) + httpClient := httputils.GetDefaultClient() + header := mcclient.GetTokenHeaders(userCred) + _, res, err := httputils.JSONRequest(httpClient, ctx, "POST", url, header, nil, false) + if err != nil { + return nil, errors.Wrap(err, "host request") + } + return res, nil +} + +func (self *SKVMGuestDriver) QgaRequestSetNetwork(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/servers/%s/qga-set-network", host.ManagerUri, guest.Id) + httpClient := httputils.GetDefaultClient() + header := mcclient.GetTokenHeaders(userCred) + _, res, err := httputils.JSONRequest(httpClient, ctx, "POST", url, header, body, false) + if err != nil { + return nil, errors.Wrap(err, "host request") + } + return res, nil +} + +func (self *SKVMGuestDriver) QgaRequestGetNetwork(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *models.SHost, guest *models.SGuest) (jsonutils.JSONObject, error) { + url := fmt.Sprintf("%s/servers/%s/qga-get-network", host.ManagerUri, guest.Id) + httpClient := httputils.GetDefaultClient() + header := mcclient.GetTokenHeaders(userCred) + _, res, err := httputils.JSONRequest(httpClient, ctx, "POST", url, header, nil, false) + if err != nil { + return nil, errors.Wrap(err, "host request") + } + return res, nil +} + func (self *SKVMGuestDriver) QgaRequestSetUserPassword(ctx context.Context, task taskman.ITask, host *models.SHost, guest *models.SGuest, input *api.ServerQgaSetPasswordInput) error { url := fmt.Sprintf("%s/servers/%s/qga-set-password", host.ManagerUri, guest.Id) httpClient := httputils.GetDefaultClient() diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index f479ecd8bf..78413c4255 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -937,6 +937,22 @@ func (self *SGuest) StartRestartNetworkTask(ctx context.Context, userCred mcclie return nil } +func (self *SGuest) StartQgaRestartNetworkTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string, device string, ipMask string, gateway string, prevIp string, inBlockStream bool) error { + data := jsonutils.NewDict() + data.Set("device", jsonutils.NewString(device)) + data.Set("ip_mask", jsonutils.NewString(ipMask)) + data.Set("gateway", jsonutils.NewString(gateway)) + data.Set("prev_ip", jsonutils.NewString(prevIp)) + data.Set("in_block_stream", jsonutils.NewBool(inBlockStream)) + if task, err := taskman.TaskManager.NewTask(ctx, "GuestQgaRestartNetworkTask", self, userCred, data, parentTaskId, "", nil); err != nil { + log.Errorln(err) + return err + } else { + task.ScheduleRun(nil) + } + return nil +} + func (self *SGuest) startSyncTask(ctx context.Context, userCred mcclient.TokenCredential, firewallOnly bool, parentTaskId string, data *jsonutils.JSONDict) error { if firewallOnly { data.Add(jsonutils.JSONTrue, "fw_only") @@ -948,15 +964,7 @@ func (self *SGuest) startSyncTask(ctx context.Context, userCred mcclient.TokenCr } func (self *SGuest) StartSyncTask(ctx context.Context, userCred mcclient.TokenCredential, firewallOnly bool, parentTaskId string) error { - - data := jsonutils.NewDict() - if firewallOnly { - data.Add(jsonutils.JSONTrue, "fw_only") - } else if err := self.SetStatus(userCred, api.VM_SYNC_CONFIG, ""); err != nil { - log.Errorln(err) - return err - } - return self.doSyncTask(ctx, data, userCred, parentTaskId) + return self.startSyncTask(ctx, userCred, firewallOnly, parentTaskId, jsonutils.NewDict()) } func (self *SGuest) StartSyncTaskWithoutSyncstatus(ctx context.Context, userCred mcclient.TokenCredential, fwOnly bool, parentTaskId string) error { @@ -2368,6 +2376,14 @@ func (self *SGuest) PerformChangeIpaddr(ctx context.Context, userCred mcclient.T return nil, err } + //Get the detailed description of the NIC + networkJsonDesc := ngn[0].getJsonDesc() + newIpAddr := networkJsonDesc.Ip + newMacAddr := networkJsonDesc.Mac + newMaskLen := networkJsonDesc.Masklen + newGateway := networkJsonDesc.Gateway + ipMask := fmt.Sprintf("%s/%d", newIpAddr, newMaskLen) + notes := jsonutils.NewDict() if gn != nil { notes.Add(jsonutils.NewString(gn.IpAddr), "prev_ip") @@ -2383,12 +2399,38 @@ func (self *SGuest) PerformChangeIpaddr(ctx context.Context, userCred mcclient.T if self.Hypervisor == api.HYPERVISOR_KVM && restartNetwork && (self.Status == api.VM_RUNNING || self.Status == api.VM_BLOCK_STREAM) { taskData.Set("restart_network", jsonutils.JSONTrue) taskData.Set("prev_ip", jsonutils.NewString(gn.IpAddr)) + taskData.Set("prev_mac", jsonutils.NewString(newMacAddr)) + net := ngn[0].GetNetwork() + taskData.Set("is_vpc_network", jsonutils.NewBool(net.isOneCloudVpcNetwork())) + taskData.Set("ip_mask", jsonutils.NewString(ipMask)) + taskData.Set("gateway", jsonutils.NewString(newGateway)) if self.Status == api.VM_BLOCK_STREAM { taskData.Set("in_block_stream", jsonutils.JSONTrue) } self.SetStatus(userCred, api.VM_RESTART_NETWORK, "restart network") } - return nil, self.startSyncTask(ctx, userCred, true, "", taskData) + return nil, self.startSyncTask(ctx, userCred, false, "", taskData) +} + +func (self *SGuest) GetIfNameByMac(ctx context.Context, userCred mcclient.TokenCredential, mac string) (string, error) { + //Find the network card according to the mac address, if it is empty, it means no network card is found + ifnameData, err := self.PerformQgaGetNetwork(ctx, userCred, nil, nil) + if err != nil { + return "", err + } + //Get the name of the network card + var parsedData []api.IfnameDetail + if err := ifnameData.Unmarshal(&parsedData); err != nil { + return "", err + } + var ifnameDevice string + //Finding a network card by its mac address + for _, detail := range parsedData { + if detail.HardwareAddress == mac { + ifnameDevice = detail.Name + } + } + return ifnameDevice, nil } func (self *SGuest) PerformDetachnetwork(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ServerDetachnetworkInput) (jsonutils.JSONObject, error) { diff --git a/pkg/compute/models/guestdrivers.go b/pkg/compute/models/guestdrivers.go index 4cb17f0c9b..1cefae8abd 100644 --- a/pkg/compute/models/guestdrivers.go +++ b/pkg/compute/models/guestdrivers.go @@ -234,6 +234,9 @@ type IGuestDriver interface { QgaRequestGuestPing(ctx context.Context, header http.Header, host *SHost, guest *SGuest, async bool, input *api.ServerQgaTimeoutInput) error QgaRequestSetUserPassword(ctx context.Context, task taskman.ITask, host *SHost, guest *SGuest, input *api.ServerQgaSetPasswordInput) error RequestQgaCommand(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *SHost, guest *SGuest) (jsonutils.JSONObject, error) + QgaRequestGuestInfoTask(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *SHost, guest *SGuest) (jsonutils.JSONObject, error) + QgaRequestSetNetwork(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *SHost, guest *SGuest) (jsonutils.JSONObject, error) + QgaRequestGetNetwork(ctx context.Context, userCred mcclient.TokenCredential, body jsonutils.JSONObject, host *SHost, guest *SGuest) (jsonutils.JSONObject, error) FetchMonitorUrl(ctx context.Context, guest *SGuest) string RequestResetNicTrafficLimit(ctx context.Context, task taskman.ITask, host *SHost, guest *SGuest, input *api.ServerNicTrafficLimit) error diff --git a/pkg/compute/models/qemu_guest_agent.go b/pkg/compute/models/qemu_guest_agent.go index 23ab9d451f..5ecfc141b0 100644 --- a/pkg/compute/models/qemu_guest_agent.go +++ b/pkg/compute/models/qemu_guest_agent.go @@ -103,3 +103,29 @@ func (self *SGuest) PerformQgaCommand( host, _ := self.GetHost() return self.GetDriver().RequestQgaCommand(ctx, userCred, jsonutils.Marshal(input), host, self) } + +func (self *SGuest) PerformQgaGuestInfoTask( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input *api.ServerQgaGuestInfoTaskInput, +) (jsonutils.JSONObject, error) { + if self.PowerStates != api.VM_POWER_STATES_ON { + return nil, httperrors.NewBadRequestError("can't use qga in vm status: %s", self.Status) + } + host, _ := self.GetHost() + return self.GetDriver().QgaRequestGuestInfoTask(ctx, userCred, nil, host, self) +} + +func (self *SGuest) PerformQgaGetNetwork( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input *api.ServerQgaGetNetworkInput, +) (jsonutils.JSONObject, error) { + if self.PowerStates != api.VM_POWER_STATES_ON { + return nil, httperrors.NewBadRequestError("can't use qga in vm status: %s", self.Status) + } + host, _ := self.GetHost() + return self.GetDriver().QgaRequestGetNetwork(ctx, userCred, nil, host, self) +} diff --git a/pkg/compute/tasks/guest_qga_restart_network_task.go b/pkg/compute/tasks/guest_qga_restart_network_task.go new file mode 100644 index 0000000000..6341b60755 --- /dev/null +++ b/pkg/compute/tasks/guest_qga_restart_network_task.go @@ -0,0 +1,93 @@ +// Copyright 2019 Yunion +// +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/util/logclient" +) + +type GuestQgaRestartNetworkTask struct { + SGuestBaseTask +} + +func init() { + taskman.RegisterTask(GuestQgaRestartNetworkTask{}) +} + +func (self *GuestQgaRestartNetworkTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { + guest := obj.(*models.SGuest) + guest.SetStatus(self.UserCred, api.VM_QGA_SET_NETWORK, "") + self.OnRestartNetwork(ctx, guest) +} + +func (self *GuestQgaRestartNetworkTask) OnRestartNetwork(ctx context.Context, guest *models.SGuest) { + device, _ := self.Params.GetString("device") + ipMask, _ := self.Params.GetString("ip_mask") + gateway, _ := self.Params.GetString("gateway") + prevIp, _ := self.Params.GetString("prev_ip") + inBlockStream, _ := self.Params.Bool("in_block_stream") + + _, err := self.requestSetNetwork(ctx, guest, device, ipMask, gateway) + //the first set maybe fail,if failed, try again + if err != nil { + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_RESTART_NETWORK, err, self.UserCred, false) + _, err = self.requestSetNetwork(ctx, guest, device, ipMask, gateway) + if err != nil { + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_RESTART_NETWORK, err, self.UserCred, false) + self.taskFailed(ctx, guest, prevIp, inBlockStream, err) + return + } + } + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_QGA_NETWORK_SUCCESS, "qga restart network success", self.UserCred, true) + guest.UpdateQgaStatus(api.QGA_STATUS_AVAILABLE) + guest.StartSyncstatus(ctx, self.UserCred, "") + self.SetStageComplete(ctx, nil) +} + +func (self *GuestQgaRestartNetworkTask) requestSetNetwork(ctx context.Context, guest *models.SGuest, device string, ipMask string, gateway string) (jsonutils.JSONObject, error) { + host, err := guest.GetHost() + if err != nil { + self.taskFailed(ctx, guest, "", false, err) + return nil, err + } + inputQgaNet := &api.ServerQgaSetNetworkInput{ + Device: device, + Ipmask: ipMask, + Gateway: gateway, + } + + // if success, log network related information + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_QGA_NETWORK_INPUT, inputQgaNet, self.UserCred, true) + return guest.GetDriver().QgaRequestSetNetwork(ctx, self.UserCred, jsonutils.Marshal(inputQgaNet), host, guest) +} + +func (self *GuestQgaRestartNetworkTask) taskFailed(ctx context.Context, guest *models.SGuest, prevIp string, inBlockStream bool, err error) { + guest.SetStatus(self.GetUserCred(), api.VM_QGA_SET_NETWORK_FAILED, err.Error()) + guest.UpdateQgaStatus(api.QGA_STATUS_EXECUTE_FAILED) + logclient.AddActionLogWithStartable(self, guest, logclient.ACT_RESTART_NETWORK, jsonutils.NewString(err.Error()), self.UserCred, false) + if prevIp != "" { + //use ansible to restart network + guest.StartRestartNetworkTask(ctx, self.UserCred, "", prevIp, inBlockStream) + } + self.SetStageFailed(ctx, nil) +} diff --git a/pkg/compute/tasks/guest_sync_task.go b/pkg/compute/tasks/guest_sync_task.go index 46894b1d1b..6ea925f368 100644 --- a/pkg/compute/tasks/guest_sync_task.go +++ b/pkg/compute/tasks/guest_sync_task.go @@ -16,14 +16,19 @@ package tasks import ( "context" + "time" "yunion.io/x/jsonutils" "yunion.io/x/log" + "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/compute/models" + "yunion.io/x/onecloud/pkg/compute/options" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules/vpcagent" "yunion.io/x/onecloud/pkg/util/logclient" ) @@ -52,24 +57,8 @@ func (self *GuestSyncConfTask) OnInit(ctx context.Context, obj db.IStandaloneMod func (self *GuestSyncConfTask) OnSyncComplete(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { guest := obj.(*models.SGuest) - if fwOnly, _ := self.GetParams().Bool("fw_only"); fwOnly { - db.OpsLog.LogEvent(guest, db.ACT_SYNC_CONF, nil, self.UserCred) - if restart, _ := self.Params.Bool("restart_network"); !restart { - self.SetStageComplete(ctx, nil) - return - } - prevIp, err := self.Params.GetString("prev_ip") - if err != nil { - log.Errorf("unable to get prev_ip when restart_network is true when sync guest") - self.SetStageComplete(ctx, nil) - return - } - if inBlockStream := jsonutils.QueryBoolean(self.Params, "in_block_stream", false); inBlockStream { - guest.StartRestartNetworkTask(ctx, self.UserCred, "", prevIp, true) - } else { - guest.StartRestartNetworkTask(ctx, self.UserCred, "", prevIp, false) - } - self.SetStageComplete(ctx, guest.GetShortDesc(ctx)) + if restart, _ := self.Params.Bool("restart_network"); restart { + self.StartRestartNetworkTask(ctx, guest) } else if data.Contains("task") { // XXX this is only applied to KVM, which will call task_complete twice self.SetStage("on_disk_sync_complete", nil) @@ -78,6 +67,67 @@ func (self *GuestSyncConfTask) OnSyncComplete(ctx context.Context, obj db.IStand } } +func (self *GuestSyncConfTask) StartRestartNetworkTask(ctx context.Context, guest *models.SGuest) { + defer self.SetStageComplete(ctx, guest.GetShortDesc(ctx)) + prevIp, err := self.Params.GetString("prev_ip") + if err != nil { + log.Errorf("unable to get prev_ip when restart_network is true when sync guest") + return + } + inBlockStream := jsonutils.QueryBoolean(self.Params, "in_block_stream", false) + if guest.Hypervisor != api.HYPERVISOR_KVM { + guest.StartRestartNetworkTask(ctx, self.UserCred, "", prevIp, inBlockStream) + return + } + preMac, err := self.Params.GetString("prev_mac") + if err != nil { + log.Errorf("unable to get prev_mac when restart_network is true when sync guest") + return + } + ipMask, err := self.Params.GetString("ip_mask") + if err != nil { + log.Errorf("unable to get ip_mask when restart_network is true when sync guest") + return + } + gateway, err := self.Params.GetString("gateway") + if err != nil { + log.Errorf("unable to get gateway when restart_network is true when sync guest") + return + } + isVpcNetwork := jsonutils.QueryBoolean(self.Params, "is_vpc_network", false) + + // try use qga restart network + err = func() error { + host, _ := guest.GetHost() + err = guest.GetDriver().QgaRequestGuestPing(ctx, self.GetTaskRequestHeader(), host, guest, false, &api.ServerQgaTimeoutInput{1000}) + if err != nil { + return errors.Wrap(err, "qga guest-ping") + } + ifnameDevice, err := guest.GetIfNameByMac(ctx, self.UserCred, preMac) + if err != nil { + return errors.Wrap(err, "get ifname by mac") + } + if ifnameDevice == "" { + return errors.Errorf("failed find ifname") + } + + if isVpcNetwork { + err = vpcagent.VpcAgent.DoSync(auth.GetAdminSession(ctx, options.Options.Region)) + if err != nil { + log.Errorf("vpcagent.VpcAgent.DoSync fail %s", err) + } + // wait for vpcagent sync network topo + time.Sleep(10 * time.Second) + } + return guest.StartQgaRestartNetworkTask( + ctx, self.UserCred, "", ifnameDevice, ipMask, gateway, prevIp, inBlockStream) + }() + if err != nil { + log.Errorf("guest %s failed start qga restart network task: %s", guest.GetName(), err) + guest.StartRestartNetworkTask(ctx, self.UserCred, "", prevIp, inBlockStream) + } +} + func (self *GuestSyncConfTask) OnDiskSyncComplete(ctx context.Context, guest *models.SGuest, data jsonutils.JSONObject) { if jsonutils.QueryBoolean(self.Params, "without_sync_status", false) { self.OnSyncStatusComplete(ctx, guest, nil) diff --git a/pkg/hostman/guestfs/core.go b/pkg/hostman/guestfs/core.go index 61ef4df878..682f03da77 100644 --- a/pkg/hostman/guestfs/core.go +++ b/pkg/hostman/guestfs/core.go @@ -129,6 +129,9 @@ func DoDeployGuestFs(rootfs fsdriver.IRootFsDriver, guestDesc *deployapi.GuestDe if err = rootfs.DeployHosts(partition, hn, domain, ips); err != nil { return nil, errors.Wrap(err, "DeployHosts") } + if err = rootfs.DeployQgaBlackList(partition); err != nil { + return nil, fmt.Errorf("DeployQgaBlackList: %v", err) + } if err = rootfs.DeployNetworkingScripts(partition, nics); err != nil { return nil, errors.Wrap(err, "DeployNetworkingScripts") } diff --git a/pkg/hostman/guestfs/fsdriver/android.go b/pkg/hostman/guestfs/fsdriver/android.go index ce9c36f63a..38a07f0c60 100644 --- a/pkg/hostman/guestfs/fsdriver/android.go +++ b/pkg/hostman/guestfs/fsdriver/android.go @@ -90,6 +90,10 @@ func (m *sBaseAndroidRootFs) DeployHosts(part IDiskPartition, hn, domain string, return nil } +func (m *sBaseAndroidRootFs) DeployQgaBlackList(part IDiskPartition) error { + return nil +} + func (m *sBaseAndroidRootFs) GetOs() string { return "Android" } diff --git a/pkg/hostman/guestfs/fsdriver/esxi.go b/pkg/hostman/guestfs/fsdriver/esxi.go index 5b859ce5f9..04012d36c5 100644 --- a/pkg/hostman/guestfs/fsdriver/esxi.go +++ b/pkg/hostman/guestfs/fsdriver/esxi.go @@ -64,6 +64,10 @@ func (m *SEsxiRootFs) DeployHostname(part IDiskPartition, hostname, domain strin return nil } +func (m *SEsxiRootFs) DeployQgaBlackList(part IDiskPartition) error { + return nil +} + func (m *SEsxiRootFs) DeployHosts(part IDiskPartition, hn, domain string, ips []string) error { return nil } diff --git a/pkg/hostman/guestfs/fsdriver/interface.go b/pkg/hostman/guestfs/fsdriver/interface.go index 24b27cb803..50494a0987 100644 --- a/pkg/hostman/guestfs/fsdriver/interface.go +++ b/pkg/hostman/guestfs/fsdriver/interface.go @@ -75,6 +75,7 @@ type IRootFsDriver interface { GetOs() string DeployHostname(part IDiskPartition, hn, domain string) error DeployHosts(part IDiskPartition, hn, domain string, ips []string) error + DeployQgaBlackList(part IDiskPartition) error DeployNetworkingScripts(IDiskPartition, []*types.SServerNic) error DeployStandbyNetworkingScripts(part IDiskPartition, nics, nicsStandby []*types.SServerNic) error DeployUdevSubsystemScripts(IDiskPartition) error diff --git a/pkg/hostman/guestfs/fsdriver/linux.go b/pkg/hostman/guestfs/fsdriver/linux.go index 8cad2e74d9..223f3e8d36 100644 --- a/pkg/hostman/guestfs/fsdriver/linux.go +++ b/pkg/hostman/guestfs/fsdriver/linux.go @@ -80,6 +80,35 @@ func getHostname(hostname, domain string) string { } } +func (l *sLinuxRootFs) DeployQgaBlackList(rootFs IDiskPartition) error { + etcSysconfigQemuga := "/etc/sysconfig/qemu-ga" + blackListContent := `# This is a systemd environment file, not a shell script. +# It provides settings for \"/lib/systemd/system/qemu-guest-agent.service\". + +# Comma-separated blacklist of RPCs to disable, or empty list to enable all. +# +# You can get the list of RPC commands using \"qemu-ga --blacklist='?'\". +# There should be no spaces between commas and commands in the blacklist. +# BLACKLIST_RPC=guest-file-open,guest-file-close,guest-file-read,guest-file-write,guest-file-seek,guest-file-flush,guest-exec,guest-exec-status + +# Fsfreeze hook script specification. +# +# FSFREEZE_HOOK_PATHNAME=/dev/null : disables the feature. +# +# FSFREEZE_HOOK_PATHNAME=/path/to/executable : enables the feature with the +# specified binary or shell script. +# +# FSFREEZE_HOOK_PATHNAME= : enables the feature with the +# default value (invoke \"qemu-ga --help\" to interrogate). +FSFREEZE_HOOK_PATHNAME=/etc/qemu-ga/fsfreeze-hook" +` + + if err := rootFs.FilePutContents(etcSysconfigQemuga, blackListContent, false, false); err != nil { + return errors.Wrap(err, "etcSysconfigQemuga error") + } + return nil +} + func (l *sLinuxRootFs) DeployHosts(rootFs IDiskPartition, hostname, domain string, ips []string) error { var etcHosts = "/etc/hosts" var oldHostFile string @@ -160,6 +189,37 @@ func (l *sLinuxRootFs) DeployPublicKey(rootFs IDiskPartition, selUsr string, pub return DeployAuthorizedKeys(rootFs, usrDir, pubkeys, false) } +func (d *SCoreOsRootFs) DeployQgaBlackList(rootFs IDiskPartition) error { + etcSysconfigQemuga := "/etc/sysconfig/qemu-ga" + blackListContent := `# This is a systemd environment file, not a shell script. +# It provides settings for \"/lib/systemd/system/qemu-guest-agent.service\". + +# Comma-separated blacklist of RPCs to disable, or empty list to enable all. +# +# You can get the list of RPC commands using \"qemu-ga --blacklist='?'\". +# There should be no spaces between commas and commands in the blacklist. +# BLACKLIST_RPC=guest-file-open,guest-file-close,guest-file-read,guest-file-write,guest-file-seek,guest-file-flush,guest-exec,guest-exec-status + +# Fsfreeze hook script specification. +# +# FSFREEZE_HOOK_PATHNAME=/dev/null : disables the feature. +# +# FSFREEZE_HOOK_PATHNAME=/path/to/executable : enables the feature with the +# specified binary or shell script. +# +# FSFREEZE_HOOK_PATHNAME= : enables the feature with the +# default value (invoke \"qemu-ga --help\" to interrogate). +FSFREEZE_HOOK_PATHNAME=/etc/qemu-ga/fsfreeze-hook" +` + + if rootFs.Exists(etcSysconfigQemuga, false) { + if err := rootFs.FilePutContents(etcSysconfigQemuga, blackListContent, false, false); err != nil { + return errors.Wrap(err, "etcSysconfigQemuga error") + } + } + return nil +} + func (l *sLinuxRootFs) DeployYunionroot(rootFs IDiskPartition, pubkeys *deployapi.SSHKeys, isInit, enableCloudInit bool) error { if !consts.AllowVmSELinux() { l.DisableSelinux(rootFs) diff --git a/pkg/hostman/guestfs/fsdriver/macos.go b/pkg/hostman/guestfs/fsdriver/macos.go index 34d19280b4..cb9ccf4db6 100644 --- a/pkg/hostman/guestfs/fsdriver/macos.go +++ b/pkg/hostman/guestfs/fsdriver/macos.go @@ -116,6 +116,10 @@ func (m *SMacOSRootFs) DeployHosts(part IDiskPartition, hn, domain string, ips [ return nil } +func (m *SMacOSRootFs) DeployQgaBlackList(part IDiskPartition) error { + return nil +} + func (m *SMacOSRootFs) GetReleaseInfo(IDiskPartition) *deployapi.ReleaseInfo { spath := "/System/Library/CoreServices/SystemVersion.plist" sInfo, _ := m.rootFs.FileGetContents(spath, false) diff --git a/pkg/hostman/guestfs/fsdriver/windows.go b/pkg/hostman/guestfs/fsdriver/windows.go index caa7de1be5..f1c1dda33b 100644 --- a/pkg/hostman/guestfs/fsdriver/windows.go +++ b/pkg/hostman/guestfs/fsdriver/windows.go @@ -277,6 +277,10 @@ func (w *SWindowsRootFs) DeployHosts(part IDiskPartition, hn, domain string, ips return w.rootFs.FilePutContents(ETC_HOSTS, hf.String(), false, true) } +func (w *SWindowsRootFs) DeployQgaBlackList(part IDiskPartition) error { + return nil +} + func (w *SWindowsRootFs) DeployNetworkingScripts(rootfs IDiskPartition, nics []*types.SServerNic) error { mainNic, err := netutils2.GetMainNicFromDeployApi(nics) if err != nil { diff --git a/pkg/hostman/guestman/guest-agent.go b/pkg/hostman/guestman/guest-agent.go index a09372a813..0b61faa03f 100644 --- a/pkg/hostman/guestman/guest-agent.go +++ b/pkg/hostman/guestman/guest-agent.go @@ -104,3 +104,59 @@ func (m *SGuestManager) QgaCommand(cmd *monitor.Command, sid string, execTimeout return string(res), err } + +func (m *SGuestManager) QgaGuestInfoTask(sid string) (string, error) { + guest, err := m.checkAndInitGuestQga(sid) + if err != nil { + return "", err + } + var res []byte + if guest.guestAgent.TryLock() { + defer guest.guestAgent.Unlock() + res, err = guest.guestAgent.GuestInfoTask() + if err != nil { + return "", errors.Wrap(err, "qga guest info task") + } + return string(res), nil + } + return "", errors.Errorf("qga unfinished last cmd, is qga unavailable?") +} + +func (m *SGuestManager) QgaSetNetwork(netmod *monitor.NetworkModify, sid string, execTimeout int) (string, error) { + guest, err := m.checkAndInitGuestQga(sid) + if err != nil { + return "", err + } + var res []byte + if guest.guestAgent.TryLock() { + defer guest.guestAgent.Unlock() + + if execTimeout > 0 { + guest.guestAgent.SetTimeout(execTimeout) + defer guest.guestAgent.ResetTimeout() + } + err = guest.guestAgent.QgaSetNetwork(netmod) + if err != nil { + return "", errors.Wrapf(err, "modify %s network failed", netmod.Device) + } + return string(res), nil + } + return "", errors.Errorf("qga unfinished last cmd, is qga unavailable?") +} + +func (m *SGuestManager) QgaGetNetwork(sid string) (string, error) { + guest, err := m.checkAndInitGuestQga(sid) + if err != nil { + return "", err + } + var res []byte + if guest.guestAgent.TryLock() { + defer guest.guestAgent.Unlock() + res, err = guest.guestAgent.QgaGetNetwork() + if err != nil { + return "", errors.Wrap(err, "qga get network fail") + } + return string(res), nil + } + return "", errors.Errorf("qga unfinished last cmd, is qga unavailable?") +} diff --git a/pkg/hostman/guestman/guesthandlers/guesthandler.go b/pkg/hostman/guestman/guesthandlers/guesthandler.go index d36226f436..7ce742b574 100644 --- a/pkg/hostman/guestman/guesthandlers/guesthandler.go +++ b/pkg/hostman/guestman/guesthandlers/guesthandler.go @@ -99,6 +99,9 @@ func AddGuestTaskHandler(prefix string, app *appsrv.Application) { "qga-command": qgaCommand, "reset-nic-traffic-limit": guestResetNicTrafficLimit, "set-nic-traffic-limit": guestSetNicTrafficLimit, + "qga-guest-info-task": qgaGuestInfoTask, + "qga-get-network": qgaGetNetwork, + "qga-set-network": qgaSetNetwork, } { app.AddHandler("POST", fmt.Sprintf("%s/%s//%s", prefix, keyWord, action), @@ -878,3 +881,38 @@ func qgaCommand(ctx context.Context, userCred mcclient.TokenCredential, sid stri return gm.QgaCommand(qgaCmd, sid, input.Timeout) } + +func qgaGuestInfoTask(ctx context.Context, userCred mcclient.TokenCredential, sid string, body jsonutils.JSONObject) (interface{}, error) { + gm := guestman.GetGuestManager() + return gm.QgaGuestInfoTask(sid) +} + +func qgaGetNetwork(ctx context.Context, userCred mcclient.TokenCredential, sid string, body jsonutils.JSONObject) (interface{}, error) { + gm := guestman.GetGuestManager() + return gm.QgaGetNetwork(sid) +} + +func qgaSetNetwork(ctx context.Context, userCred mcclient.TokenCredential, sid string, body jsonutils.JSONObject) (interface{}, error) { + gm := guestman.GetGuestManager() + input := computeapi.ServerQgaSetNetworkInput{} + err := body.Unmarshal(&input) + if err != nil { + return nil, httperrors.NewInputParameterError("unmarshal input to ServerQgaSetNetworkInput: %s", err.Error()) + } + if input.Device == "" { + return nil, httperrors.NewMissingParameterError("device") + } + if input.Ipmask == "" { + return nil, httperrors.NewMissingParameterError("ipmask") + } + if input.Gateway == "" { + return nil, httperrors.NewMissingParameterError("gateway") + } + + qgaNetMod := &monitor.NetworkModify{ + Device: input.Device, + Ipmask: input.Ipmask, + Gateway: input.Gateway, + } + return gm.QgaSetNetwork(qgaNetMod, sid, input.Timeout) +} diff --git a/pkg/hostman/guestman/qemu-kvm.go b/pkg/hostman/guestman/qemu-kvm.go index 43ae971792..1f5b660ec0 100644 --- a/pkg/hostman/guestman/qemu-kvm.go +++ b/pkg/hostman/guestman/qemu-kvm.go @@ -2025,6 +2025,7 @@ func getNicBridge(nic *desc.SGuestNetwork) string { } func onNicChange(oldNic, newNic *desc.SGuestNetwork) error { + log.Infof("nic changed old: %s new: %s", jsonutils.Marshal(oldNic), jsonutils.Marshal(newNic)) // override network base desc oldNic.GuestnetworkBaseDesc = newNic.GuestnetworkBaseDesc diff --git a/pkg/hostman/monitor/qga/qga.go b/pkg/hostman/monitor/qga/qga.go index adff58c236..88ccc2e285 100644 --- a/pkg/hostman/monitor/qga/qga.go +++ b/pkg/hostman/monitor/qga/qga.go @@ -18,7 +18,10 @@ import ( "bufio" "encoding/base64" "encoding/json" + "fmt" "net" + "strconv" + "strings" "sync" "sync/atomic" "time" @@ -173,7 +176,7 @@ func (qga *QemuGuestAgent) execCmd(cmd *monitor.Command, expectResp bool, readTi return nil, nil } - if readTimeout < 0 { + if readTimeout <= 0 { readTimeout = qga.readTimeout } err = qga.rwc.SetReadDeadline(time.Now().Add(time.Duration(readTimeout) * time.Millisecond)) @@ -245,6 +248,217 @@ func (qga *QemuGuestAgent) GuestInfo() (*GuestInfo, error) { return res, nil } +func (qga *QemuGuestAgent) GuestInfoTask() ([]byte, error) { + info, err := qga.GuestInfo() + if err != nil { + return nil, err + } + cmd := &monitor.Command{ + Execute: "guest-info", + } + var i = 0 + for ; i < len(info.SupportedCommands); i++ { + if info.SupportedCommands[i].Name == cmd.Execute { + break + } + } + if i > len(info.SupportedCommands) { + return nil, errors.Errorf("unsupported command %s", cmd.Execute) + } + if !info.SupportedCommands[i].Enabled { + return nil, errors.Errorf("command %s not enabled", cmd.Execute) + } + res, err := qga.execCmd(cmd, true, -1) + if err != nil { + return nil, err + } + return *res, nil +} + +func (qga *QemuGuestAgent) QgaGetNetwork() ([]byte, error) { + cmd := &monitor.Command{ + Execute: "guest-network-get-interfaces", + } + res, err := qga.execCmd(cmd, true, -1) + if err != nil { + return nil, err + } + return *res, nil +} + +type GuestOsInfo struct { + Id string `json:"id"` + KernelRelease string `json:"kernel-release"` + KernelVersion string `json:"kernel-version"` + Machine string `json:"machine"` + Name string `json:"name"` + PrettyName string `json:"pretty-name"` + Version string `json:"version"` + VersionId string `json:"version-id"` +} + +func (qga *QemuGuestAgent) QgaGuestGetOsInfo() (*GuestOsInfo, error) { + //run guest-get-osinfo + cmdOsInfo := &monitor.Command{ + Execute: "guest-get-osinfo", + } + rawResOsInfo, err := qga.execCmd(cmdOsInfo, true, -1) + resOsInfo := new(GuestOsInfo) + err = json.Unmarshal(*rawResOsInfo, resOsInfo) + if err != nil { + return nil, errors.Wrap(err, "unmarshal raw response") + } + return resOsInfo, nil +} + +func (qga *QemuGuestAgent) QgaFileOpen(path string) (int, error) { + //file open + cmdFileOpen := &monitor.Command{ + Execute: "guest-file-open", + Args: map[string]interface{}{ + "path": path, + "mode": "w+", + }, + } + rawResFileOpen, err := qga.execCmd(cmdFileOpen, true, -1) + if err != nil { + return 0, err + } + fileNum, err := strconv.ParseInt(string(*rawResFileOpen), 10, 64) + if err != nil { + return 0, err + } + return int(fileNum), nil +} + +func (qga *QemuGuestAgent) QgaFileWrite(fileNum int, content string) error { + contentEncode := base64.StdEncoding.EncodeToString([]byte(content)) + //write shell to file + cmdFileWrite := &monitor.Command{ + Execute: "guest-file-write", + Args: map[string]interface{}{ + "handle": fileNum, + "buf-b64": contentEncode, + }, + } + _, err := qga.execCmd(cmdFileWrite, true, -1) + if err != nil { + return err + } + return nil +} + +func (qga *QemuGuestAgent) QgaFileClose(fileNum int) error { + //close file + cmdFileClose := &monitor.Command{ + Execute: "guest-file-close", + Args: map[string]interface{}{ + "handle": fileNum, + }, + } + _, err := qga.execCmd(cmdFileClose, true, -1) + if err != nil { + return err + } + return nil +} + +func ParseIPAndSubnet(input string) (string, string, error) { + //Converting IP/MASK format to IP and MASK + parts := strings.Split(input, "/") + if len(parts) != 2 { + return "", "", fmt.Errorf("Invalid input format") + } + + ip := parts[0] + subnetSizeStr := parts[1] + + subnetSize := 0 + for _, c := range subnetSizeStr { + if c < '0' || c > '9' { + return "", "", fmt.Errorf("Invalid subnet size") + } + subnetSize = subnetSize*10 + int(c-'0') + } + + mask := net.CIDRMask(subnetSize, 32) + subnetMask := net.IP(mask).To4().String() + return ip, subnetMask, nil +} + +func (qga *QemuGuestAgent) QgaAddFileExec(filePath string) error { + //Adding execution permissions to file + shellAddAuth := "chmod +x " + filePath + arg := []string{"-c", shellAddAuth} + cmdAddAuth := &monitor.Command{ + Execute: "guest-exec", + Args: map[string]interface{}{ + "path": "/bin/bash", + "arg": arg, + "env": []string{}, + "input-data": "", + "capture-output": true, + }, + } + _, err := qga.execCmd(cmdAddAuth, true, -1) + if err != nil { + return err + } + return nil +} + +func (qga *QemuGuestAgent) QgaSetWindowsNetwork(qgaNetMod *monitor.NetworkModify) error { + ip, subnetMask, err := ParseIPAndSubnet(qgaNetMod.Ipmask) + if err != nil { + return err + } + networkCmd := fmt.Sprintf( + "netsh interface ip set address name=\"%s\" source=static addr=%s mask=%s gateway=%s & "+ + "netsh interface ip set address name=\"%s\" dhcp", + qgaNetMod.Device, ip, subnetMask, qgaNetMod.Gateway, qgaNetMod.Device, + ) + + log.Infof("networkCmd: %s", networkCmd) + arg := []string{"/C", networkCmd} + cmdExecNet := &monitor.Command{ + Execute: "guest-exec", + Args: map[string]interface{}{ + "path": "C:\\Windows\\System32\\cmd.exe", + "arg": arg, + "env": []string{}, + "input-data": "", + "capture-output": true, + }, + } + _, err = qga.execCmd(cmdExecNet, true, -1) + if err != nil { + return err + } + return nil +} + +func (qga *QemuGuestAgent) QgaSetLinuxNetwork(qgaNetMod *monitor.NetworkModify) error { + args := []string{"-c", fmt.Sprintf("/sbin/dhclient -r %s && /sbin/dhclient -1 %s", qgaNetMod.Device, qgaNetMod.Device)} + _, err := qga.GuestExecCommand("/bin/bash", args, []string{}, "", false) + return err +} + +func (qga *QemuGuestAgent) QgaSetNetwork(qgaNetMod *monitor.NetworkModify) error { + //Getting information about the operating system + resOsInfo, err := qga.QgaGuestGetOsInfo() + if err != nil { + return errors.Wrap(err, "get os info") + } + + //Judgement based on id, currently only windows and other systems are judged + switch resOsInfo.Id { + case "mswindows": + return qga.QgaSetWindowsNetwork(qgaNetMod) + default: + return qga.QgaSetLinuxNetwork(qgaNetMod) + } +} + /* # @username: the user account whose password to change # @password: the new password entry string, base64 encoded diff --git a/pkg/hostman/monitor/qmp.go b/pkg/hostman/monitor/qmp.go index 0e22c488b8..524a8b7fd6 100644 --- a/pkg/hostman/monitor/qmp.go +++ b/pkg/hostman/monitor/qmp.go @@ -82,6 +82,12 @@ type Command struct { Args interface{} `json:"arguments,omitempty"` } +type NetworkModify struct { + Device string `json:"device"` + Ipmask string `json:"ipmask"` + Gateway string `json:"gateway"` +} + type Version struct { Package string `json:"package"` QEMU struct { diff --git a/pkg/mcclient/options/compute/servers.go b/pkg/mcclient/options/compute/servers.go index 0e94ba6c3f..42d5de9a6b 100644 --- a/pkg/mcclient/options/compute/servers.go +++ b/pkg/mcclient/options/compute/servers.go @@ -857,6 +857,22 @@ func (o *ServerQgaPing) Params() (jsonutils.JSONObject, error) { return options.StructToParams(o) } +type ServerQgaGuestInfoTask struct { + ServerIdOptions +} + +func (o *ServerQgaGuestInfoTask) Params() (jsonutils.JSONObject, error) { + return options.StructToParams(o) +} + +type ServerQgaGetNetwork struct { + ServerIdOptions +} + +func (o *ServerQgaGetNetwork) Params() (jsonutils.JSONObject, error) { + return options.StructToParams(o) +} + type ServerSetPasswordOptions struct { ServerIdOptions diff --git a/pkg/util/logclient/consts.go b/pkg/util/logclient/consts.go index 0a3134a669..6b79c42949 100644 --- a/pkg/util/logclient/consts.go +++ b/pkg/util/logclient/consts.go @@ -227,6 +227,10 @@ const ( ACT_RESTART_NETWORK = "restart_network" + ACT_QGA_NETWORK_INPUT = "qga_network_input" + ACT_QGA_STATUS_UPDATE = "qga_status_update" + ACT_QGA_NETWORK_SUCCESS = "qga_network_success" + ACT_RECOVERY = "recovery" ACT_PACK = "pack" ACT_UNPACK = "unpack"