diff --git a/cmd/climc/shell/compute/servers.go b/cmd/climc/shell/compute/servers.go index b9a4523288..6a44065c88 100644 --- a/cmd/climc/shell/compute/servers.go +++ b/cmd/climc/shell/compute/servers.go @@ -87,6 +87,7 @@ func init() { cmd.Perform("remote-update", new(options.ServerRemoteUpdateOptions)) cmd.Perform("create-eip", &options.ServerCreateEipOptions{}) cmd.Perform("make-sshable", &options.ServerMakeSshableOptions{}) + cmd.Perform("migrate-network", &options.ServerMigrateNetworkOptions{}) cmd.Get("vnc", new(options.ServerIdOptions)) cmd.Get("desc", new(options.ServerIdOptions)) diff --git a/pkg/apis/compute/guests.go b/pkg/apis/compute/guests.go index cfc697c396..32c9e0bc35 100644 --- a/pkg/apis/compute/guests.go +++ b/pkg/apis/compute/guests.go @@ -549,3 +549,10 @@ type ServerResizeDiskInput struct { DiskResizeInput } + +type ServerMigrateNetworkInput struct { + // Source network Id + Src string `json:"src"` + // Destination network Id + Dest string `json:"dest"` +} diff --git a/pkg/compute/models/guest_migrate_network.go b/pkg/compute/models/guest_migrate_network.go new file mode 100644 index 0000000000..6379136fef --- /dev/null +++ b/pkg/compute/models/guest_migrate_network.go @@ -0,0 +1,149 @@ +// 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 models + +import ( + "context" + "database/sql" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/netutils" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" +) + +func (guest *SGuest) AllowPerformMigrateNetwork( + ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input api.ServerMigrateNetworkInput, +) bool { + return db.IsAdminAllowPerform(userCred, guest, "migrate-network") +} + +/* + * Migrate a server from one network to another network, without change IP address + * Scenerio: the server used to be in a VPC, migrate it to a underlay network without network interruption + */ +func (guest *SGuest) PerformMigrateNetwork(ctx context.Context, userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input api.ServerMigrateNetworkInput, +) (jsonutils.JSONObject, error) { + if guest.Hypervisor != api.HYPERVISOR_KVM { + return nil, errors.Wrap(httperrors.ErrNotSupported, "operation not supported for this hypervisor") + } + + // first validate it against the source network, ensure the following: + // 1. the network is attach to this guest + srcModel, err := NetworkManager.FetchByIdOrName(userCred, input.Src) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, errors.Wrapf(httperrors.ErrResourceNotFound, "source network %s not found", input.Src) + } else { + return nil, errors.Wrap(err, "NetworkManager.FetchByIdOrName") + } + } + srcNics, err := guest.GetNetworks(srcModel.GetId()) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, errors.Wrapf(httperrors.ErrBadRequest, "server not attach to network %s", input.Src) + } else { + return nil, errors.Wrap(err, "GetNetworks") + } + } + if len(srcNics) == 0 { + return nil, errors.Wrapf(httperrors.ErrBadRequest, "server not attach to network %s", input.Src) + } else if len(srcNics) > 1 { + return nil, errors.Wrapf(httperrors.ErrNotSupported, "not support to migrate multiple interfaces") + } + srcNic := srcNics[0] + ipAddr, err := netutils.NewIPV4Addr(srcNic.IpAddr) + if err != nil { + return nil, errors.Wrapf(err, "NewIPV4Addr") + } + // next validate against the destination network, ensure the following: + // 1. the network is reachable to this server + // 1. the IP address is availalble in the new network + destModel, err := NetworkManager.FetchByIdOrName(userCred, input.Dest) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, errors.Wrapf(httperrors.ErrResourceNotFound, "destination network %s not found", input.Src) + } else { + return nil, errors.Wrap(err, "NetworkManager.FetchByIdOrName") + } + } + destNet := destModel.(*SNetwork) + + host := guest.GetHost() + if host == nil { + return nil, errors.Wrap(httperrors.ErrInvalidStatus, "guest is not allocated!") + } + if destNet.isOneCloudVpcNetwork() { + // vpc network should be in the same Zone + destZone := destNet.GetZone() + if destZone == nil || destZone.Id != host.ZoneId { + return nil, errors.Wrap(httperrors.ErrBadRequest, "destination overlay network not in same zone as server") + } + } else { + // underlay network should be reachable in wire + var destWire *SWire + wires := host.getAttachedWires() + for i := range wires { + if wires[i].Id == destNet.WireId { + // reachable + destWire = &wires[i] + break + } + } + if destWire == nil { + return nil, errors.Wrap(httperrors.ErrBadRequest, "destination underlay network not reachable") + } + } + + lockman.LockObject(ctx, destNet) + defer lockman.ReleaseObject(ctx, destNet) + + if !destNet.IsAddressInRange(ipAddr) { + return nil, errors.Wrapf(httperrors.ErrBadRequest, "ip %s not in range of destination network", ipAddr.String()) + } + used, err := destNet.isAddressUsed(ipAddr.String()) + if err != nil { + return nil, errors.Wrap(err, "isAddressUsed") + } + if used { + return nil, errors.Wrapf(httperrors.ErrBadRequest, "ip %s has been allocated in destination network", ipAddr.String()) + } + + // perform the database change + _, err = db.Update(&srcNic, func() error { + srcNic.NetworkId = destNet.Id + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "fail to update nic network_id") + } + + // synchronize the change to host, and wait it to be effective + err = guest.StartSyncTask(ctx, userCred, true, ":") + if err != nil { + return nil, errors.Wrap(err, "fail to SyncTask") + } + + return nil, nil +} diff --git a/pkg/mcclient/options/servers.go b/pkg/mcclient/options/servers.go index d7babe322d..5f6c5dd8a8 100644 --- a/pkg/mcclient/options/servers.go +++ b/pkg/mcclient/options/servers.go @@ -1122,3 +1122,13 @@ func (opts *ServerMakeSshableOptions) Params() (jsonutils.JSONObject, error) { } return jsonutils.Marshal(opts), nil } + +type ServerMigrateNetworkOptions struct { + BaseIdOptions + + computeapi.ServerMigrateNetworkInput +} + +func (opts *ServerMigrateNetworkOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(opts), nil +}