proxmox.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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 proxmox
  15. import (
  16. "bytes"
  17. "context"
  18. "crypto/tls"
  19. "fmt"
  20. "io"
  21. "mime/multipart"
  22. "net/http"
  23. "net/url"
  24. "path/filepath"
  25. "strings"
  26. "time"
  27. "yunion.io/x/jsonutils"
  28. "yunion.io/x/log"
  29. "yunion.io/x/pkg/errors"
  30. "yunion.io/x/pkg/util/httputils"
  31. api "yunion.io/x/cloudmux/pkg/apis/compute"
  32. "yunion.io/x/cloudmux/pkg/cloudprovider"
  33. )
  34. const (
  35. CLOUD_PROVIDER_PROXMOX = api.CLOUD_PROVIDER_PROXMOX
  36. AUTH_ADDR = "/access/ticket"
  37. )
  38. type SProxmoxClient struct {
  39. *ProxmoxClientConfig
  40. }
  41. type ProxmoxClientConfig struct {
  42. cpcfg cloudprovider.ProviderConfig
  43. username string
  44. password string
  45. host string
  46. authURL string
  47. port int
  48. csrfToken string
  49. authTicket string // Combination of user, realm, token ID and UUID
  50. debug bool
  51. }
  52. func NewProxmoxClientConfig(username, password, host string, port int) *ProxmoxClientConfig {
  53. cfg := &ProxmoxClientConfig{
  54. username: username,
  55. password: password,
  56. host: host,
  57. authURL: fmt.Sprintf("https://%s:%d/api2/json", host, port),
  58. port: port,
  59. }
  60. return cfg
  61. }
  62. func (self *ProxmoxClientConfig) Debug(debug bool) *ProxmoxClientConfig {
  63. self.debug = debug
  64. return self
  65. }
  66. func (self *ProxmoxClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *ProxmoxClientConfig {
  67. self.cpcfg = cpcfg
  68. return self
  69. }
  70. func NewProxmoxClient(cfg *ProxmoxClientConfig) (*SProxmoxClient, error) {
  71. client := &SProxmoxClient{
  72. ProxmoxClientConfig: cfg,
  73. }
  74. return client, client.auth()
  75. }
  76. func (self *SProxmoxClient) auth() error {
  77. params := map[string]interface{}{
  78. "username": self.username,
  79. "password": self.password,
  80. }
  81. ret, err := self.__jsonRequest(httputils.POST, AUTH_ADDR, params)
  82. if err != nil {
  83. return errors.Wrapf(err, "post")
  84. }
  85. dat, err := ret.Get("data")
  86. if err != nil {
  87. return errors.Wrapf(err, "decode data")
  88. }
  89. if ticket, err := dat.GetString("ticket"); err != nil {
  90. return errors.Wrapf(err, "get ticket")
  91. } else {
  92. self.authTicket = ticket
  93. }
  94. if token, err := dat.GetString("CSRFPreventionToken"); err != nil {
  95. return errors.Wrapf(err, "get Token")
  96. } else {
  97. self.csrfToken = token
  98. }
  99. return nil
  100. }
  101. func (self *SProxmoxClient) GetRegion() *SRegion {
  102. region := &SRegion{client: self}
  103. return region
  104. }
  105. func (self *SProxmoxClient) GetRegions() ([]SRegion, error) {
  106. ret := []SRegion{}
  107. ret = append(ret, SRegion{client: self})
  108. return ret, nil
  109. }
  110. type ProxmoxError struct {
  111. Url string
  112. Message string
  113. Code int
  114. Params []string
  115. Errors string
  116. Status string
  117. }
  118. func (self ProxmoxError) Error() string {
  119. return jsonutils.Marshal(self).String()
  120. }
  121. func (ce *ProxmoxError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
  122. ce.Status = status
  123. if body != nil {
  124. body.Unmarshal(ce)
  125. log.Errorf("%s status: %s(%d) error: %v", ce.Url, status, statusCode, body.PrettyString())
  126. }
  127. if ce.Code == 0 && statusCode > 0 {
  128. ce.Code = statusCode
  129. }
  130. if ce.Code == 404 {
  131. return errors.Wrap(cloudprovider.ErrNotFound, ce.Error())
  132. }
  133. return ce
  134. }
  135. func (cli *SProxmoxClient) getDefaultClient() *http.Client {
  136. client := httputils.GetAdaptiveTimeoutClient()
  137. httputils.SetClientProxyFunc(client, cli.cpcfg.ProxyFunc)
  138. ts, _ := client.Transport.(*http.Transport)
  139. ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
  140. client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) {
  141. if cli.cpcfg.ReadOnly {
  142. if req.Method == "GET" || req.Method == "HEAD" {
  143. return nil, nil
  144. }
  145. // 认证
  146. if req.Method == "POST" && strings.HasSuffix(req.URL.Path, "/access/ticket") {
  147. return nil, nil
  148. }
  149. return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
  150. }
  151. return nil, nil
  152. })
  153. return client
  154. }
  155. func (cli *SProxmoxClient) post(res string, params interface{}) (jsonutils.JSONObject, error) {
  156. resp, err := cli._jsonRequest(httputils.POST, res, params)
  157. if err != nil {
  158. return resp, err
  159. }
  160. ret := struct {
  161. TaskId string
  162. }{}
  163. resp.Unmarshal(&ret)
  164. if len(ret.TaskId) > 0 && ret.TaskId != "null" {
  165. _, err = cli.waitTask(ret.TaskId)
  166. return resp, err
  167. }
  168. return resp, nil
  169. }
  170. func (cli *SProxmoxClient) put(res string, params url.Values, body jsonutils.JSONObject) error {
  171. if params != nil {
  172. res = fmt.Sprintf("%s?%s", res, params.Encode())
  173. }
  174. resp, err := cli._jsonRequest(httputils.PUT, res, body)
  175. if err != nil {
  176. return err
  177. }
  178. ret := struct {
  179. TaskId string
  180. }{}
  181. resp.Unmarshal(&ret)
  182. if len(ret.TaskId) > 0 && ret.TaskId != "null" {
  183. _, err = cli.waitTask(ret.TaskId)
  184. return err
  185. }
  186. return nil
  187. }
  188. func (cli *SProxmoxClient) get(res string, params url.Values, retVal interface{}) error {
  189. if len(params) > 0 {
  190. res = fmt.Sprintf("%s?%s", res, params.Encode())
  191. }
  192. resp, err := cli._jsonRequest(httputils.GET, res, nil)
  193. if err != nil {
  194. return err
  195. }
  196. dat, err := resp.Get("data")
  197. if err != nil {
  198. return errors.Wrapf(err, "decode data")
  199. }
  200. return dat.Unmarshal(retVal)
  201. }
  202. func (cli *SProxmoxClient) getAgent(res string, params url.Values, retVal interface{}) error {
  203. resp, err := cli._jsonRequest(httputils.GET, res, nil)
  204. if err != nil {
  205. return err
  206. }
  207. dat, err := resp.Get("data")
  208. if err != nil {
  209. return errors.Wrapf(err, "decode data")
  210. }
  211. ret, err := dat.Get("result")
  212. if err != nil {
  213. return errors.Wrapf(err, "decode data")
  214. }
  215. return ret.Unmarshal(retVal)
  216. }
  217. func (cli *SProxmoxClient) del(res string, params url.Values, retVal interface{}) error {
  218. if params != nil {
  219. res = fmt.Sprintf("%s?%s", res, params.Encode())
  220. }
  221. resp, err := cli._jsonRequest(httputils.DELETE, res, nil)
  222. if err != nil {
  223. return err
  224. }
  225. taskId, err := resp.GetString("data")
  226. if err != nil {
  227. return err
  228. }
  229. _, err = cli.waitTask(taskId)
  230. return err
  231. }
  232. func (cli *SProxmoxClient) _jsonRequest(method httputils.THttpMethod, res string, params interface{}) (jsonutils.JSONObject, error) {
  233. ret, err := cli.__jsonRequest(method, res, params)
  234. if err != nil {
  235. if e, ok := err.(*ProxmoxError); ok && e.Code == 401 {
  236. cli.auth()
  237. return cli.__jsonRequest(method, res, params)
  238. }
  239. return ret, err
  240. }
  241. return ret, nil
  242. }
  243. func (cli *SProxmoxClient) __jsonRequest(method httputils.THttpMethod, res string, params interface{}) (jsonutils.JSONObject, error) {
  244. client := httputils.NewJsonClient(cli.getDefaultClient())
  245. url := fmt.Sprintf("%s/%s", cli.authURL, strings.TrimPrefix(res, "/"))
  246. req := httputils.NewJsonRequest(method, url, params)
  247. header := http.Header{}
  248. if len(cli.csrfToken) > 0 && len(cli.csrfToken) > 0 && res != AUTH_ADDR {
  249. header.Set("Cookie", "PVEAuthCookie="+cli.authTicket)
  250. header.Set("CSRFPreventionToken", cli.csrfToken)
  251. }
  252. req.SetHeader(header)
  253. oe := &ProxmoxError{Url: url}
  254. _, resp, err := client.Send(context.Background(), req, oe, cli.debug)
  255. if err != nil {
  256. return nil, err
  257. }
  258. return resp, nil
  259. }
  260. func (cli *SProxmoxClient) upload(node, storageName, filename string, reader io.Reader) (*SImage, error) {
  261. if !strings.HasSuffix(filename, ".iso") {
  262. filename = filename + ".iso"
  263. }
  264. filename = filepath.Base(filename)
  265. client := cli.getDefaultClient()
  266. res := fmt.Sprintf("/nodes/%s/storage/%s/upload", node, storageName)
  267. url := fmt.Sprintf("%s/%s", cli.authURL, strings.TrimPrefix(res, "/"))
  268. body := &bytes.Buffer{}
  269. writer := multipart.NewWriter(body)
  270. err := writer.WriteField("content", "iso")
  271. if err != nil {
  272. return nil, err
  273. }
  274. part, err := writer.CreateFormFile("filename", filename)
  275. if err != nil {
  276. return nil, err
  277. }
  278. _, err = io.Copy(part, reader)
  279. if err != nil {
  280. return nil, errors.Wrapf(err, "io.Copy")
  281. }
  282. writer.Close()
  283. req, err := http.NewRequest("POST", url, body)
  284. if err != nil {
  285. return nil, err
  286. }
  287. req.Header.Set("Content-Type", writer.FormDataContentType())
  288. if len(cli.csrfToken) > 0 && len(cli.csrfToken) > 0 && res != AUTH_ADDR {
  289. req.Header.Set("Cookie", "PVEAuthCookie="+cli.authTicket)
  290. req.Header.Set("CSRFPreventionToken", cli.csrfToken)
  291. }
  292. resp, err := client.Do(req)
  293. if err != nil {
  294. return nil, err
  295. }
  296. defer resp.Body.Close()
  297. data, err := io.ReadAll(resp.Body)
  298. if err != nil {
  299. return nil, err
  300. }
  301. obj, err := jsonutils.Parse(data)
  302. if err != nil {
  303. return nil, err
  304. }
  305. if obj.Contains("errors") {
  306. return nil, fmt.Errorf("%s", string(data))
  307. }
  308. now := time.Now()
  309. for now.Sub(time.Now()) < time.Minute*1 {
  310. images, err := cli.GetImages(node, storageName)
  311. if err != nil {
  312. return nil, errors.Wrapf(err, "GetImageStatus")
  313. }
  314. for i := range images {
  315. if strings.HasSuffix(images[i].Volid, filename) {
  316. return &images[i], nil
  317. }
  318. }
  319. time.Sleep(time.Second * 10)
  320. }
  321. return nil, errors.Wrapf(cloudprovider.ErrNotFound, "after upload")
  322. }
  323. func (cli *SProxmoxClient) GetCloudRegionExternalIdPrefix() string {
  324. return fmt.Sprintf("%s/%s", CLOUD_PROVIDER_PROXMOX, cli.cpcfg.Id)
  325. }
  326. func (self *SProxmoxClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
  327. subAccount := cloudprovider.SSubAccount{}
  328. subAccount.Id = self.host
  329. subAccount.Name = self.cpcfg.Name
  330. subAccount.Account = self.username
  331. subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
  332. return []cloudprovider.SSubAccount{subAccount}, nil
  333. }
  334. func (self *SProxmoxClient) GetAccountId() string {
  335. return self.host
  336. }
  337. func (self *SProxmoxClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) {
  338. ret := []cloudprovider.ICloudRegion{}
  339. region := self.GetRegion()
  340. ret = append(ret, region)
  341. return ret, nil
  342. }
  343. func (self *SProxmoxClient) GetCapabilities() []string {
  344. ret := []string{
  345. cloudprovider.CLOUD_CAPABILITY_COMPUTE,
  346. cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX,
  347. }
  348. return ret
  349. }