ssh.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package utils
  15. import (
  16. "fmt"
  17. "net"
  18. "strings"
  19. "time"
  20. "yunion.io/x/jsonutils"
  21. "yunion.io/x/log"
  22. "yunion.io/x/pkg/errors"
  23. cloudproxy_api "yunion.io/x/onecloud/pkg/apis/cloudproxy"
  24. comapi "yunion.io/x/onecloud/pkg/apis/compute"
  25. "yunion.io/x/onecloud/pkg/mcclient"
  26. "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
  27. modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
  28. )
  29. var ErrServerNotSshable = errors.Error("server is not sshable")
  30. type SSHable struct {
  31. Ok bool
  32. Reason string
  33. User string
  34. Host string
  35. Port int
  36. Password string
  37. ServerName string
  38. ServerHypervisor string
  39. OsType string
  40. ProxyEndpointId string
  41. ProxyAgentId string
  42. ProxyForwardId string
  43. }
  44. func checkSshableForOtherCloud(session *mcclient.ClientSession, serverId string) (SSHable, error) {
  45. data, err := modules.Servers.GetSpecific(session, serverId, "sshable", nil)
  46. if err != nil {
  47. return SSHable{}, errors.Wrapf(err, "unable to get sshable info of server %s", serverId)
  48. }
  49. log.Infof("data to sshable: %v", data)
  50. var sshableOutput comapi.GuestSshableOutput
  51. err = data.Unmarshal(&sshableOutput)
  52. if err != nil {
  53. return SSHable{}, errors.Wrapf(err, "unable to marshal output of server sshable: %s", data)
  54. }
  55. sshable := SSHable{
  56. User: sshableOutput.User,
  57. }
  58. reasons := make([]string, 0, len(sshableOutput.MethodTried))
  59. for _, methodTried := range sshableOutput.MethodTried {
  60. if !methodTried.Sshable {
  61. reasons = append(reasons, methodTried.Reason)
  62. continue
  63. }
  64. sshable.Ok = true
  65. switch methodTried.Method {
  66. case comapi.MethodDirect, comapi.MethodEIP, comapi.MethodDNAT:
  67. sshable.Host = methodTried.Host
  68. sshable.Port = methodTried.Port
  69. case comapi.MethodProxyForward:
  70. sshable.ProxyAgentId = methodTried.ForwardDetails.ProxyAgentId
  71. sshable.ProxyEndpointId = methodTried.ForwardDetails.ProxyEndpointId
  72. }
  73. }
  74. if !sshable.Ok {
  75. sshable.Reason = strings.Join(reasons, "; ")
  76. }
  77. return sshable, nil
  78. }
  79. type SServerInfo struct {
  80. Id string
  81. Ip string
  82. VpcId string
  83. Hypervisor string
  84. }
  85. func CheckSshableForYunionCloud(session *mcclient.ClientSession, serverInfo SServerInfo) (sshable SSHable, cleanFunc func() error, err error) {
  86. sshable, clean, err := checkSshableForYunionCloud(session, serverInfo, false)
  87. if err != nil {
  88. return
  89. }
  90. if clean {
  91. cleanFunc = func() error {
  92. proxyAddr := sshable.Host
  93. proxyPort := sshable.Port
  94. params := jsonutils.NewDict()
  95. params.Set("proto", jsonutils.NewString("tcp"))
  96. params.Set("proxy_addr", jsonutils.NewString(proxyAddr))
  97. params.Set("proxy_port", jsonutils.NewInt(int64(proxyPort)))
  98. _, err := modules.Servers.PerformAction(session, serverInfo.Id, "close-forward", params)
  99. if err != nil {
  100. return errors.Wrapf(err, "unable to close forward(addr %q, port %d, proto %q) for server %s", proxyAddr, proxyPort, "tcp", serverInfo.Id)
  101. }
  102. return nil
  103. }
  104. }
  105. return
  106. }
  107. func getLoginInfo(session *mcclient.ClientSession, serverId string) (username string, password string, err error) {
  108. ret, err := modules.Servers.GetLoginInfo(session, serverId, jsonutils.NewDict())
  109. if err != nil {
  110. return
  111. }
  112. username, _ = ret.GetString("username")
  113. password, _ = ret.GetString("password")
  114. return
  115. }
  116. func checkSshableForYunionCloud(session *mcclient.ClientSession, serverInfo SServerInfo, isWindows bool) (sshable SSHable, clean bool, err error) {
  117. port, err := getServerSshport(session, serverInfo.Id, isWindows)
  118. if err != nil {
  119. err = errors.Wrapf(err, "unable to get ssh port of server %s", serverInfo.Id)
  120. return
  121. }
  122. ip := serverInfo.Ip
  123. username, password := "cloudroot", ""
  124. if isWindows {
  125. username, password, err = getLoginInfo(session, serverInfo.Id)
  126. if err != nil {
  127. return
  128. }
  129. sshable.OsType = "Windows"
  130. }
  131. sshable.User = username
  132. sshable.Password = password
  133. sshable.Port = port
  134. sshable.Host = ip
  135. if serverInfo.Hypervisor == comapi.HYPERVISOR_BAREMETAL || serverInfo.VpcId == "" || serverInfo.VpcId == comapi.DEFAULT_VPC_ID {
  136. sshable.Ok = true
  137. return
  138. }
  139. lfParams := jsonutils.NewDict()
  140. lfParams.Set("proto", jsonutils.NewString("tcp"))
  141. lfParams.Set("port", jsonutils.NewInt(int64(port)))
  142. lfParams.Set("addr", jsonutils.NewString(ip))
  143. data, err := modules.Servers.PerformAction(session, serverInfo.Id, "list-forward", lfParams)
  144. if err != nil {
  145. err = errors.Wrapf(err, "unable to List Forward for server %s", serverInfo.Id)
  146. return
  147. }
  148. var openForward bool
  149. var forwards []jsonutils.JSONObject
  150. if !data.Contains("forwards") {
  151. openForward = true
  152. } else {
  153. forwards, err = data.GetArray("forwards")
  154. if err != nil {
  155. err = errors.Wrap(err, "parse response of List Forward")
  156. return
  157. }
  158. openForward = len(forwards) == 0
  159. }
  160. var forward jsonutils.JSONObject
  161. if openForward {
  162. forward, err = modules.Servers.PerformAction(session, serverInfo.Id, "open-forward", lfParams)
  163. if err != nil {
  164. err = errors.Wrapf(err, "unable to Open Forward for server %s", serverInfo.Id)
  165. return
  166. }
  167. clean = true
  168. } else {
  169. forward = forwards[0]
  170. }
  171. proxyAddr, _ := forward.GetString("proxy_addr")
  172. proxyPort, _ := forward.Int("proxy_port")
  173. // register
  174. sshable.Port = int(proxyPort)
  175. sshable.Host = proxyAddr
  176. sshable.Ok = true
  177. return
  178. }
  179. func checkSshableForYunionCloudWithDetail(session *mcclient.ClientSession, serverDetail *comapi.ServerDetails) (sshable SSHable, clean bool, err error) {
  180. if serverDetail.IPs == "" {
  181. err = fmt.Errorf("empty ips for server %s", serverDetail.Id)
  182. return
  183. }
  184. isWindows := false
  185. if serverDetail.OsType == "Windows" {
  186. isWindows = true
  187. }
  188. ips := strings.Split(serverDetail.IPs, ",")
  189. ip := strings.TrimSpace(ips[0])
  190. return checkSshableForYunionCloud(session, SServerInfo{
  191. Ip: ip,
  192. Id: serverDetail.Id,
  193. Hypervisor: serverDetail.Hypervisor,
  194. VpcId: serverDetail.VpcId,
  195. }, isWindows)
  196. }
  197. func CheckSSHable(session *mcclient.ClientSession, serverId string) (sshable SSHable, cleanFunc func() error, err error) {
  198. params := jsonutils.NewDict()
  199. params.Set("details", jsonutils.JSONTrue)
  200. data, err := modules.Servers.GetById(session, serverId, params)
  201. if err != nil {
  202. err = errors.Wrapf(err, "unable to fetch server %s", serverId)
  203. return
  204. }
  205. var serverDetail comapi.ServerDetails
  206. err = data.Unmarshal(&serverDetail)
  207. if err != nil {
  208. err = errors.Wrapf(err, "unable to unmarshal %q to ServerDetails", data)
  209. return
  210. }
  211. // check sshable
  212. var clean bool
  213. if serverDetail.Hypervisor == comapi.HYPERVISOR_KVM || serverDetail.Hypervisor == comapi.HYPERVISOR_BAREMETAL {
  214. sshable, clean, err = checkSshableForYunionCloudWithDetail(session, &serverDetail)
  215. if err != nil {
  216. return
  217. }
  218. if clean {
  219. cleanFunc = func() error {
  220. proxyAddr := sshable.Host
  221. proxyPort := sshable.Port
  222. params := jsonutils.NewDict()
  223. params.Set("proto", jsonutils.NewString("tcp"))
  224. params.Set("proxy_addr", jsonutils.NewString(proxyAddr))
  225. params.Set("proxy_port", jsonutils.NewInt(int64(proxyPort)))
  226. _, err := modules.Servers.PerformAction(session, serverDetail.Id, "close-forward", params)
  227. if err != nil {
  228. return errors.Wrapf(err, "unable to close forward(addr %q, port %d, proto %q) for server %s", proxyAddr, proxyPort, "tcp", serverDetail.Id)
  229. }
  230. return nil
  231. }
  232. }
  233. } else {
  234. sshable, err = checkSshableForOtherCloud(session, serverDetail.Id)
  235. if err != nil {
  236. return
  237. }
  238. }
  239. if !sshable.Ok {
  240. err = ErrServerNotSshable
  241. if len(sshable.Reason) > 0 {
  242. err = errors.Wrap(err, sshable.Reason)
  243. }
  244. return
  245. }
  246. sshable.ServerName = serverDetail.Name
  247. sshable.ServerHypervisor = serverDetail.Hypervisor
  248. // make sure user
  249. if sshable.User == "" {
  250. switch {
  251. case serverDetail.Hypervisor == comapi.HYPERVISOR_KVM:
  252. sshable.User = "root"
  253. default:
  254. sshable.User = "cloudroot"
  255. }
  256. }
  257. var forwardId string
  258. if len(sshable.ProxyEndpointId) == 0 {
  259. return
  260. } else {
  261. var sshport int
  262. sshport, err = getServerSshport(session, serverDetail.Id, false)
  263. if err != nil {
  264. err = errors.Wrapf(err, "unable to get sshport of server %s", serverDetail.Id)
  265. }
  266. // create local forward
  267. createP := jsonutils.NewDict()
  268. createP.Set("type", jsonutils.NewString(cloudproxy_api.FORWARD_TYPE_LOCAL))
  269. createP.Set("remote_port", jsonutils.NewInt(int64(sshport)))
  270. createP.Set("server_id", jsonutils.NewString(serverDetail.Id))
  271. var forward jsonutils.JSONObject
  272. forward, err = cloudproxy.Forwards.PerformClassAction(session, "create-from-server", createP)
  273. if err != nil {
  274. err = errors.Wrapf(err, "fail to create local forward from server %q", serverDetail.Id)
  275. return
  276. }
  277. cleanFunc = func() error {
  278. return clearLocalForward(session, forwardId)
  279. }
  280. var agent jsonutils.JSONObject
  281. port, _ := forward.Int("bind_port")
  282. forwardId, _ = forward.GetString("id")
  283. agentId, _ := forward.GetString("proxy_agent_id")
  284. agent, err = cloudproxy.ProxyAgents.Get(session, agentId, nil)
  285. if err != nil {
  286. err = errors.Wrapf(err, "fail to get proxy agent %q", agentId)
  287. return
  288. }
  289. address, _ := agent.GetString("advertise_addr")
  290. // check proxy forward
  291. if ok := ensureLocalForwardWork(address, int(port)); !ok {
  292. err = errors.Error("The created local forward is actually not usable")
  293. return
  294. }
  295. sshable.Host = address
  296. sshable.Port = int(port)
  297. sshable.ProxyForwardId = forwardId
  298. }
  299. return
  300. }
  301. func GetCleanFunc(session *mcclient.ClientSession, hypervisor, serverId, host, forward string, port int) func() error {
  302. if hypervisor == comapi.HYPERVISOR_KVM || hypervisor == comapi.HYPERVISOR_BAREMETAL {
  303. return func() error {
  304. proxyAddr := host
  305. proxyPort := port
  306. params := jsonutils.NewDict()
  307. params.Set("proto", jsonutils.NewString("tcp"))
  308. params.Set("proxy_addr", jsonutils.NewString(proxyAddr))
  309. params.Set("proxy_port", jsonutils.NewInt(int64(proxyPort)))
  310. _, err := modules.Servers.PerformAction(session, serverId, "close-forward", params)
  311. if err != nil {
  312. return errors.Wrapf(err, "unable to close forward(addr %q, port %d, proto %q) for server %s", proxyAddr, proxyPort, "tcp", serverId)
  313. }
  314. return nil
  315. }
  316. }
  317. return func() error {
  318. return clearLocalForward(session, forward)
  319. }
  320. }
  321. func getServerSshport(session *mcclient.ClientSession, serverId string, isWindows bool) (int, error) {
  322. if isWindows {
  323. return 5985, nil
  324. }
  325. data, err := modules.Servers.GetSpecific(session, serverId, "sshport", nil)
  326. if err != nil {
  327. return 0, err
  328. }
  329. port, _ := data.Int("port")
  330. if port == 0 {
  331. port = 22
  332. }
  333. return int(port), nil
  334. }
  335. func clearLocalForward(s *mcclient.ClientSession, forwardId string) error {
  336. if len(forwardId) == 0 {
  337. return nil
  338. }
  339. _, err := cloudproxy.Forwards.Delete(s, forwardId, nil)
  340. return err
  341. }
  342. func ensureLocalForwardWork(host string, port int) bool {
  343. maxWaitTimes, wt := 10, 1*time.Second
  344. waitTimes := 1
  345. address := fmt.Sprintf("%s:%d", host, port)
  346. for waitTimes < maxWaitTimes {
  347. _, err := net.DialTimeout("tcp", address, 1*time.Second)
  348. if err == nil {
  349. return true
  350. }
  351. log.Debugf("no.%d times, try to connect to %s failed: %s", waitTimes, address, err)
  352. time.Sleep(wt)
  353. waitTimes += 1
  354. wt += 1 * time.Second
  355. }
  356. return false
  357. }