diff --git a/pkg/cloudproxy/models/proxy_endpoints.go b/pkg/cloudproxy/models/proxy_endpoints.go index f5ee1fd68d..ecb7433fb5 100644 --- a/pkg/cloudproxy/models/proxy_endpoints.go +++ b/pkg/cloudproxy/models/proxy_endpoints.go @@ -109,9 +109,15 @@ func (man *SProxyEndpointManager) PerformCreateFromServer(ctx context.Context, u IntranetIpAddr: nic.IpAddr, } + proxyendpoint.SetModelManager(man, proxyendpoint) proxyendpoint.Name = name proxyendpoint.DomainId = serverInfo.Server.DomainId proxyendpoint.ProjectId = serverInfo.Server.ProjectId + + if err := proxyendpoint.remoteCheckMake(ctx, userCred); err != nil { + return nil, err + } + if err := man.TableSpec().Insert(ctx, proxyendpoint); err != nil { return nil, httperrors.NewServerError("database insertion error: %v", err) } @@ -183,6 +189,11 @@ func (man *SProxyEndpointManager) ValidateCreateData( return data, nil } +func (proxyendpoint *SProxyEndpoint) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + err := proxyendpoint.remoteCheckMake(ctx, userCred) + return err +} + func (man *SProxyEndpointManager) getById(id string) (*SProxyEndpoint, error) { m, err := db.FetchById(man, id) if err != nil { diff --git a/pkg/cloudproxy/models/proxy_endpoints_remote_check_make.go b/pkg/cloudproxy/models/proxy_endpoints_remote_check_make.go new file mode 100644 index 0000000000..99b46937a7 --- /dev/null +++ b/pkg/cloudproxy/models/proxy_endpoints_remote_check_make.go @@ -0,0 +1,167 @@ +// 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" + "fmt" + "math/rand" + "net" + "time" + + "yunion.io/x/log" + + cloudproxy_api "yunion.io/x/onecloud/pkg/apis/cloudproxy" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + mcclient_module "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/util/ansible" + ssh_util "yunion.io/x/onecloud/pkg/util/ssh" +) + +func (proxyendpoint *SProxyEndpoint) remoteCheckMake(ctx context.Context, userCred mcclient.TokenCredential) error { + ctx, _ = context.WithTimeout(ctx, 7*time.Second) + conf := ssh_util.ClientConfig{ + Username: proxyendpoint.User, + Host: proxyendpoint.Host, + Port: proxyendpoint.Port, + PrivateKey: proxyendpoint.PrivateKey, + } + client, err := conf.ConnectContext(ctx) + if err != nil { + return httperrors.NewBadRequestError("ssh connect failed: %v", err) + } + defer client.Close() + + if err := proxyendpoint.remoteConfigure(ctx, userCred); err != nil { + return err + } + + // total wait time + tmo := time.NewTimer(23 * time.Second) + tmoErr := httperrors.NewOutOfResourceError("timeout testing remote config. please retry later") + + // find a remote port for bind and listen + var ( + listenAddr string + listener net.Listener + ) + portMin := cloudproxy_api.BindPortMax + 1 + portTotal := 65535 - cloudproxy_api.BindPortMax + portReqStart := rand.Intn(portTotal) + for portInc := portReqStart; ; { + port := portMin + portInc + addr := net.JoinHostPort(proxyendpoint.IntranetIpAddr, fmt.Sprintf("%d", port)) + listener_, err := client.Listen("tcp", addr) + if err == nil { + // we assume the port is not occupied by intranet addr, + // even though there is the possibility that the above + // test only happen against 127.0.0.1 + listenAddr = addr + listener = listener_ + break + } + select { + case <-tmo.C: + return tmoErr + default: + } + portInc += 1 + if portInc == portTotal { + portInc = 0 + } + if portInc == portReqStart { + return httperrors.NewOutOfResourceError("no available port for bind test") + } + } + + // test for good news + for { + if listener != nil { + if conn, err := client.Dial("tcp", listenAddr); err == nil { + conn.Close() + listener.Close() + return nil + } + listener.Close() + } + + var err error + listener, err = client.Listen("tcp", listenAddr) + if err != nil { + log.Warningf("retry ssh listen %s: %v", listenAddr, err) + } + select { + case <-tmo.C: + return tmoErr + case <-time.After(2 * time.Second): + } + } + return httperrors.NewConflictError("remote sshd_config may have a problem with GatewayPorts") +} + +func (proxyendpoint *SProxyEndpoint) remoteConfigure(ctx context.Context, userCred mcclient.TokenCredential) error { + host := ansible.Host{ + Name: "0.0.0.0", // ansibleserver requires this field to be either ip_addr, or name of guest, host + } + host.SetVar("ansible_user", proxyendpoint.User) + host.SetVar("ansible_host", proxyendpoint.Host) + host.SetVar("ansible_port", fmt.Sprintf("%d", proxyendpoint.Port)) + host.SetVar("ansible_become", "yes") + pb := &ansible.Playbook{ + PrivateKey: []byte(proxyendpoint.PrivateKey), + Inventory: ansible.Inventory{ + Hosts: []ansible.Host{host}, + }, + Modules: []ansible.Module{ + { + Name: "lineinfile", + Args: []string{ + "dest=/etc/ssh/sshd_config", + "state=present", + fmt.Sprintf("regexp=%q", "^GatewayPorts "), + fmt.Sprintf("line=%q", "GatewayPorts clientspecified"), + fmt.Sprintf("validate=%q", "sshd -T -f %s"), + }, + }, + { + Name: "service", + Args: []string{ + "name=sshd", + "state=restarted", + }, + }, + { + Name: "service", + Args: []string{ + "name=ssh", // ubuntu uses this name. It can fail for centos, but we do not care + "state=restarted", + }, + }, + }, + } + + cliSess := auth.GetSession(ctx, userCred, "", "") + pbId := "" + pbName := "pe-remote-configure-" + proxyendpoint.Name + _, err := mcclient_module.AnsiblePlaybooks.UpdateOrCreatePbModel( + ctx, cliSess, pbId, pbName, pb, + ) + if err != nil { + return httperrors.NewGeneralError(err) + } + return nil +}