guest_sshable.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  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 models
  15. import (
  16. "context"
  17. "fmt"
  18. "strconv"
  19. "strings"
  20. "time"
  21. "yunion.io/x/jsonutils"
  22. "yunion.io/x/log"
  23. "yunion.io/x/pkg/errors"
  24. "yunion.io/x/pkg/tristate"
  25. "yunion.io/x/pkg/util/httputils"
  26. "yunion.io/x/sqlchemy"
  27. cloudproxy_api "yunion.io/x/onecloud/pkg/apis/cloudproxy"
  28. compute_api "yunion.io/x/onecloud/pkg/apis/compute"
  29. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  30. "yunion.io/x/onecloud/pkg/compute/sshkeys"
  31. "yunion.io/x/onecloud/pkg/httperrors"
  32. "yunion.io/x/onecloud/pkg/mcclient"
  33. "yunion.io/x/onecloud/pkg/mcclient/auth"
  34. ansible_modules "yunion.io/x/onecloud/pkg/mcclient/modules/ansible"
  35. cloudproxy_module "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
  36. "yunion.io/x/onecloud/pkg/util/ansible"
  37. "yunion.io/x/onecloud/pkg/util/logclient"
  38. ssh_util "yunion.io/x/onecloud/pkg/util/ssh"
  39. )
  40. type GuestSshableTryData struct {
  41. DryRun bool
  42. User string
  43. Host string
  44. Port int
  45. PrivateKey string
  46. PublicKey string
  47. MethodTried []compute_api.GuestSshableMethodData
  48. }
  49. func (tryData *GuestSshableTryData) AddMethodTried(tryMethodData compute_api.GuestSshableMethodData) {
  50. tryData.MethodTried = append(tryData.MethodTried, tryMethodData)
  51. }
  52. func (tryData *GuestSshableTryData) outputJSON() jsonutils.JSONObject {
  53. out := compute_api.GuestSshableOutput{
  54. User: tryData.User,
  55. PublicKey: tryData.PublicKey,
  56. MethodTried: tryData.MethodTried,
  57. }
  58. outJSON := jsonutils.Marshal(out)
  59. return outJSON
  60. }
  61. func (guest *SGuest) GetDetailsSshable(
  62. ctx context.Context,
  63. userCred mcclient.TokenCredential,
  64. query jsonutils.JSONObject,
  65. ) (jsonutils.JSONObject, error) {
  66. if guest.Status != compute_api.VM_RUNNING {
  67. return nil, httperrors.NewBadRequestError("server sshable state can only be checked when in running state")
  68. }
  69. tryData := &GuestSshableTryData{
  70. User: "cloudroot",
  71. }
  72. // - get admin key
  73. privateKey, publicKey, err := sshkeys.GetSshAdminKeypair(ctx)
  74. if err != nil {
  75. return nil, httperrors.NewInternalServerError("fetch ssh private key: %v", err)
  76. }
  77. tryData.PrivateKey = privateKey
  78. tryData.PublicKey = publicKey
  79. if err := guest.sshableTryEach(ctx, userCred, tryData); err != nil {
  80. return nil, err
  81. }
  82. {
  83. sshable := false
  84. for i := range tryData.MethodTried {
  85. if tryData.MethodTried[i].Sshable {
  86. sshable = true
  87. break
  88. }
  89. }
  90. if _, err := db.Update(guest, func() error {
  91. guest.SshableLastState = tristate.NewFromBool(sshable)
  92. return nil
  93. }); err != nil {
  94. log.Errorf("update guest %s(%s) sshable_last_state to %v: %v", guest.Name, guest.Id, sshable, err)
  95. }
  96. }
  97. logclient.AddActionLogWithContext(ctx, guest, logclient.ACT_TRYSSHABLE, nil, userCred, true)
  98. return tryData.outputJSON(), nil
  99. }
  100. func (guest *SGuest) sshableTryEach(
  101. ctx context.Context,
  102. userCred mcclient.TokenCredential,
  103. tryData *GuestSshableTryData,
  104. ) error {
  105. gns, err := guest.GetNetworks("")
  106. if err != nil {
  107. return httperrors.NewInternalServerError("fetch network interface information: %v", err)
  108. }
  109. type gnInfo struct {
  110. guestNetwork *SGuestnetwork
  111. network *SNetwork
  112. vpc *SVpc
  113. }
  114. // make sure the ssh port
  115. var sshPort int
  116. if tryData.Port != 0 {
  117. sshPort = tryData.Port
  118. } else {
  119. sshPort = guest.GetSshPort(ctx, userCred)
  120. }
  121. tryData.Port = sshPort
  122. var gnInfos []gnInfo
  123. for i := range gns {
  124. gn := &gns[i]
  125. network, _ := gn.GetNetwork()
  126. if network == nil {
  127. continue
  128. }
  129. vpc, _ := network.GetVpc()
  130. if vpc == nil {
  131. continue
  132. }
  133. if vpc.Id == compute_api.DEFAULT_VPC_ID || vpc.Direct {
  134. if ok := guest.sshableTryDefaultVPC(ctx, tryData, gn); ok {
  135. return nil
  136. }
  137. } else {
  138. gnInfos = append(gnInfos, gnInfo{
  139. guestNetwork: gn,
  140. network: network,
  141. vpc: vpc,
  142. })
  143. }
  144. }
  145. // - check eip
  146. if eip, err := guest.GetEipOrPublicIp(); err == nil && eip != nil {
  147. if ok := guest.sshableTryEip(ctx, tryData, eip); ok {
  148. return nil
  149. }
  150. }
  151. sess := auth.GetSession(ctx, userCred, "")
  152. // - check existing proxy forward
  153. proxyforwardTried := false
  154. for i := range gnInfos {
  155. gnInfo := &gnInfos[i]
  156. gn := gnInfo.guestNetwork
  157. port := sshPort
  158. input := &cloudproxy_api.ForwardListInput{
  159. Type: cloudproxy_api.FORWARD_TYPE_LOCAL,
  160. RemoteAddr: gn.IpAddr,
  161. RemotePort: &port,
  162. Opaque: guest.Id,
  163. }
  164. params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
  165. params.Set("details", jsonutils.JSONTrue)
  166. res, err := cloudproxy_module.Forwards.List(sess, params)
  167. if err != nil {
  168. log.Warningf("list cloudproxy forwards: %v", err)
  169. continue
  170. }
  171. proxyforwardTried = len(res.Data) != 0
  172. for _, data := range res.Data {
  173. var fwd cloudproxy_api.ForwardDetails
  174. if err := data.Unmarshal(&fwd); err != nil {
  175. log.Warningf("unmarshal cloudproxy forward list data: %v", err)
  176. continue
  177. }
  178. if ok := guest.sshableTryForward(ctx, tryData, &fwd); ok {
  179. return nil
  180. }
  181. }
  182. }
  183. if !proxyforwardTried {
  184. // - create and use new proxy forward
  185. fwdCreateInput := cloudproxy_api.ForwardCreateFromServerInput{
  186. ServerId: guest.Id,
  187. Type: cloudproxy_api.FORWARD_TYPE_LOCAL,
  188. RemotePort: sshPort,
  189. }
  190. fwdCreateParams := jsonutils.Marshal(fwdCreateInput)
  191. res, err := cloudproxy_module.Forwards.PerformClassAction(sess, "create-from-server", fwdCreateParams)
  192. if err == nil {
  193. var fwd cloudproxy_api.ForwardDetails
  194. if err := res.Unmarshal(&fwd); err != nil {
  195. log.Errorf("unmarshal fwd details: %q", res.String())
  196. }
  197. tmo := time.NewTimer(13 * time.Second)
  198. tick := time.NewTicker(3 * time.Second)
  199. out:
  200. for {
  201. select {
  202. case <-tmo.C:
  203. break out
  204. case <-tick.C:
  205. if ok := guest.sshableTryForward(ctx, tryData, &fwd); ok {
  206. return nil
  207. }
  208. case <-ctx.Done():
  209. break
  210. }
  211. }
  212. } else {
  213. var reason string
  214. if jce, ok := err.(*httputils.JSONClientError); ok {
  215. reason = jce.Details
  216. } else {
  217. reason = err.Error()
  218. }
  219. tryData.AddMethodTried(compute_api.GuestSshableMethodData{
  220. Method: compute_api.MethodProxyForward,
  221. Reason: reason,
  222. })
  223. }
  224. }
  225. // - existing dnat rule
  226. for i := range gnInfos {
  227. gnInfo := &gnInfos[i]
  228. gn := gnInfo.guestNetwork
  229. vpc := gnInfo.vpc
  230. natgwq := NatGatewayManager.Query().SubQuery()
  231. q := NatDEntryManager.Query().
  232. Equals("internal_ip", gn.IpAddr).
  233. Equals("internal_port", sshPort).
  234. Equals("ip_protocol", "tcp")
  235. q = q.Join(natgwq, sqlchemy.AND(
  236. sqlchemy.In(natgwq.Field("vpc_id"), vpc.Id),
  237. sqlchemy.Equals(natgwq.Field("id"), q.Field("natgateway_id")),
  238. ))
  239. var dnats []SNatDEntry
  240. if err := db.FetchModelObjects(NatDEntryManager, q, &dnats); err != nil {
  241. log.Warningf("query dnat to ssh service: %v", err)
  242. continue
  243. }
  244. for j := range dnats {
  245. dnat := &dnats[j]
  246. if ok := guest.sshableTryDnat(ctx, tryData, dnat); ok {
  247. return nil
  248. }
  249. }
  250. }
  251. return nil
  252. }
  253. func (guest *SGuest) sshableTryDnat(
  254. ctx context.Context,
  255. tryData *GuestSshableTryData,
  256. dnat *SNatDEntry,
  257. ) bool {
  258. methodData := compute_api.GuestSshableMethodData{
  259. Method: compute_api.MethodDNAT,
  260. Host: dnat.ExternalIP,
  261. Port: dnat.ExternalPort,
  262. }
  263. return guest.sshableTry(
  264. ctx, tryData, methodData,
  265. )
  266. }
  267. func (guest *SGuest) sshableTryForward(
  268. ctx context.Context,
  269. tryData *GuestSshableTryData,
  270. fwd *cloudproxy_api.ForwardDetails,
  271. ) bool {
  272. if fwd.BindAddr != "" && fwd.BindPort > 0 {
  273. methodData := compute_api.GuestSshableMethodData{
  274. Method: compute_api.MethodProxyForward,
  275. Host: fwd.BindAddr,
  276. Port: fwd.BindPort,
  277. ForwardDetails: compute_api.ForwardDetails{
  278. ProxyAgentId: fwd.ProxyAgentId,
  279. ProxyEndpointId: fwd.ProxyEndpointId,
  280. },
  281. }
  282. return guest.sshableTry(
  283. ctx, tryData, methodData,
  284. )
  285. }
  286. return false
  287. }
  288. func (guest *SGuest) sshableTryEip(
  289. ctx context.Context,
  290. tryData *GuestSshableTryData,
  291. eip *SElasticip,
  292. ) bool {
  293. methodData := compute_api.GuestSshableMethodData{
  294. Method: compute_api.MethodEIP,
  295. Host: eip.IpAddr,
  296. Port: tryData.Port,
  297. }
  298. return guest.sshableTry(
  299. ctx, tryData, methodData,
  300. )
  301. }
  302. func (guest *SGuest) sshableTryDefaultVPC(
  303. ctx context.Context,
  304. tryData *GuestSshableTryData,
  305. gn *SGuestnetwork,
  306. ) bool {
  307. methodData := compute_api.GuestSshableMethodData{
  308. Method: compute_api.MethodDirect,
  309. Host: gn.IpAddr,
  310. Port: tryData.Port,
  311. }
  312. return guest.sshableTry(
  313. ctx, tryData, methodData,
  314. )
  315. }
  316. func (guest *SGuest) sshableTry(
  317. ctx context.Context,
  318. tryData *GuestSshableTryData,
  319. methodData compute_api.GuestSshableMethodData,
  320. ) bool {
  321. if tryData.DryRun {
  322. tryData.AddMethodTried(methodData)
  323. return true
  324. }
  325. ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
  326. defer cancel()
  327. conf := ssh_util.ClientConfig{
  328. Username: tryData.User,
  329. Host: methodData.Host,
  330. Port: methodData.Port,
  331. PrivateKey: tryData.PrivateKey,
  332. }
  333. ok := false
  334. if client, err := conf.ConnectContext(ctx); err == nil {
  335. defer client.Close()
  336. methodData.Sshable = true
  337. ok = true
  338. } else {
  339. methodData.Reason = err.Error()
  340. }
  341. tryData.AddMethodTried(methodData)
  342. return ok
  343. }
  344. func (guest *SGuest) PerformHaveAgent(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input compute_api.GuestHaveAgentInput) (compute_api.GuestHaveAgentOutput, error) {
  345. var output compute_api.GuestHaveAgentOutput
  346. v := guest.GetMetadata(ctx, "__monitor_agent", userCred)
  347. if v == "true" {
  348. output.Have = true
  349. return output, nil
  350. }
  351. v = guest.GetMetadata(ctx, "sys:monitor_agent", userCred)
  352. if v == "true" {
  353. output.Have = true
  354. return output, nil
  355. }
  356. v = guest.GetMetadata(ctx, "telegraf_deployed", userCred)
  357. if v == "true" {
  358. output.Have = true
  359. return output, nil
  360. }
  361. return output, nil
  362. }
  363. func (guest *SGuest) PerformMakeSshable(
  364. ctx context.Context,
  365. userCred mcclient.TokenCredential,
  366. query jsonutils.JSONObject,
  367. input compute_api.GuestMakeSshableInput,
  368. ) (output compute_api.GuestMakeSshableOutput, err error) {
  369. if guest.Status != compute_api.VM_RUNNING {
  370. return output, httperrors.NewBadRequestError("make-sshable can only be performed when in running state")
  371. }
  372. if input.User == "" {
  373. return output, httperrors.NewBadRequestError("missing username")
  374. }
  375. if input.PrivateKey == "" && input.Password == "" {
  376. return output, httperrors.NewBadRequestError("private_key and password cannot both be empty")
  377. }
  378. _, projectPublicKey, err := sshkeys.GetSshProjectKeypair(ctx, guest.ProjectId)
  379. if err != nil {
  380. return output, httperrors.NewInternalServerError("fetch project public key: %v", err)
  381. }
  382. _, adminPublicKey, err := sshkeys.GetSshAdminKeypair(ctx)
  383. if err != nil {
  384. return output, httperrors.NewInternalServerError("fetch admin public key: %v", err)
  385. }
  386. tryData := &GuestSshableTryData{
  387. DryRun: true,
  388. Port: input.Port,
  389. }
  390. if err := guest.sshableTryEach(ctx, userCred, tryData); err != nil {
  391. return output, httperrors.NewNotAcceptableError("searching for usable ssh address: %v", err)
  392. } else if len(tryData.MethodTried) == 0 {
  393. return output, httperrors.NewNotAcceptableError("no usable ssh address")
  394. }
  395. // storage sshport
  396. if input.Port != 0 {
  397. err := guest.SetSshPort(ctx, userCred, input.Port)
  398. if err != nil {
  399. return output, errors.Wrap(err, "unable to set sshport for guest")
  400. }
  401. }
  402. host := ansible.Host{
  403. Name: guest.Name,
  404. }
  405. host.SetVar("ansible_user", input.User)
  406. host.SetVar("ansible_host", tryData.MethodTried[0].Host)
  407. host.SetVar("ansible_port", fmt.Sprintf("%d", tryData.MethodTried[0].Port))
  408. host.SetVar("ansible_become", "yes")
  409. pb := &ansible.Playbook{
  410. Inventory: ansible.Inventory{
  411. Hosts: []ansible.Host{host},
  412. },
  413. Modules: []ansible.Module{
  414. {
  415. Name: "group",
  416. Args: []string{
  417. "name=cloudroot",
  418. "state=present",
  419. },
  420. },
  421. {
  422. Name: "user",
  423. Args: []string{
  424. "name=cloudroot",
  425. "state=present",
  426. "group=cloudroot",
  427. },
  428. },
  429. {
  430. Name: "authorized_key",
  431. Args: []string{
  432. "user=cloudroot",
  433. "state=present",
  434. fmt.Sprintf("key=%q", adminPublicKey),
  435. },
  436. },
  437. {
  438. Name: "authorized_key",
  439. Args: []string{
  440. "user=cloudroot",
  441. "state=present",
  442. fmt.Sprintf("key=%q", projectPublicKey),
  443. },
  444. },
  445. {
  446. Name: "lineinfile",
  447. Args: []string{
  448. "dest=/etc/sudoers",
  449. "state=present",
  450. fmt.Sprintf("regexp=%q", "^cloudroot "),
  451. fmt.Sprintf("line=%q", "cloudroot ALL=(ALL) NOPASSWD: ALL"),
  452. fmt.Sprintf("validate=%q", "visudo -cf %s"),
  453. },
  454. },
  455. },
  456. }
  457. if input.PrivateKey != "" {
  458. pb.PrivateKey = []byte(input.PrivateKey)
  459. } else if input.Password != "" {
  460. host.SetVar("ansible_password", input.Password)
  461. }
  462. cliSess := auth.GetSession(ctx, userCred, "")
  463. pbId := ""
  464. pbName := "make-sshable-" + guest.Id
  465. pbModel, err := ansible_modules.AnsiblePlaybooks.UpdateOrCreatePbModel(
  466. ctx, cliSess, pbId, pbName, pb,
  467. )
  468. if err != nil {
  469. return output, httperrors.NewGeneralError(err)
  470. }
  471. logclient.AddActionLogWithContext(ctx, guest, logclient.ACT_MAKESSHABLE, nil, userCred, true)
  472. output = compute_api.GuestMakeSshableOutput{
  473. AnsiblePlaybookId: pbModel.Id,
  474. }
  475. return output, nil
  476. }
  477. func (guest *SGuest) GetDetailsMakeSshableCmd(
  478. ctx context.Context,
  479. userCred mcclient.TokenCredential,
  480. query jsonutils.JSONObject,
  481. ) (output compute_api.GuestMakeSshableCmdOutput, err error) {
  482. _, projectPublicKey, err := sshkeys.GetSshProjectKeypair(ctx, guest.ProjectId)
  483. if err != nil {
  484. return output, httperrors.NewInternalServerError("fetch project public key: %v", err)
  485. }
  486. _, adminPublicKey, err := sshkeys.GetSshAdminKeypair(ctx)
  487. if err != nil {
  488. return output, httperrors.NewInternalServerError("fetch admin public key: %v", err)
  489. }
  490. varVals := [][2]string{
  491. {"user", "cloudroot"},
  492. {"adminpub", strings.TrimSpace(adminPublicKey)},
  493. {"projpub", strings.TrimSpace(projectPublicKey)},
  494. }
  495. shellCmd := ""
  496. for i := range varVals {
  497. varVal := varVals[i]
  498. shellCmd += fmt.Sprintf("%s=%q\n", varVal[0], varVal[1])
  499. }
  500. shellCmd += `
  501. group="$user"
  502. sshdir="/home/$user/.ssh"
  503. keyfile="$sshdir/authorized_keys"
  504. `
  505. shellCmd += `
  506. id -g "$group" &>/dev/null || groupadd "$group"
  507. id -u "$user" &>/dev/null || useradd --create-home --gid "$group" "$user"
  508. mkdir -p "$sshdir"
  509. grep -q -F "$adminpub" "$keyfile" &>/dev/null || echo "$adminpub" >>"$keyfile"
  510. grep -q -F "$projpub" "$keyfile" &>/dev/null || echo "$projpub" >>"$keyfile"
  511. chown -R "$user:$group" "$sshdir"
  512. chmod -R 700 "$sshdir"
  513. chmod -R 600 "$keyfile"
  514. if ! grep -q "^$user " /etc/sudoers; then
  515. echo "$user ALL=(ALL) NOPASSWD: ALL" | EDITOR='tee -a' visudo
  516. fi
  517. `
  518. output = compute_api.GuestMakeSshableCmdOutput{
  519. ShellCmd: shellCmd,
  520. }
  521. return output, nil
  522. }
  523. func (guest *SGuest) GetSshPort(ctx context.Context, userCred mcclient.TokenCredential) int {
  524. portStr := guest.GetMetadata(ctx, compute_api.SSH_PORT, userCred)
  525. if portStr == "" {
  526. return 22
  527. }
  528. port, _ := strconv.Atoi(portStr)
  529. return port
  530. }
  531. func (guest *SGuest) SetSshPort(ctx context.Context, userCred mcclient.TokenCredential, port int) error {
  532. return guest.SetMetadata(ctx, compute_api.SSH_PORT, port, userCred)
  533. }
  534. func (guest *SGuest) PerformSetSshport(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input compute_api.GuestSetSshportInput) (jsonutils.JSONObject, error) {
  535. if input.Port < 0 {
  536. return nil, httperrors.NewInputParameterError("invalid port")
  537. }
  538. return nil, guest.SetSshPort(ctx, userCred, input.Port)
  539. }
  540. func (guest *SGuest) GetDetailsSshport(
  541. ctx context.Context,
  542. userCred mcclient.TokenCredential,
  543. query jsonutils.JSONObject,
  544. ) (compute_api.GuestSshportOutput, error) {
  545. port := guest.GetSshPort(ctx, userCred)
  546. return compute_api.GuestSshportOutput{Port: port}, nil
  547. }