client.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. package linode
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "strings"
  9. "time"
  10. "git.aetherial.dev/aeth/yosai/pkg/config"
  11. daemonproto "git.aetherial.dev/aeth/yosai/pkg/daemon-proto"
  12. "git.aetherial.dev/aeth/yosai/pkg/keytags"
  13. "git.aetherial.dev/aeth/yosai/pkg/secrets/keyring"
  14. )
  15. const LinodeApiUrl = "api.linode.com"
  16. const LinodeInstances = "linode/instances"
  17. const LinodeImages = "images"
  18. const LinodeApiVers = "v4"
  19. const LinodeRegions = "regions"
  20. const LinodeTypes = "linode/types"
  21. const (
  22. DEFAULT_TYPE = "g6-nanode-1"
  23. DEFAULT_IMAGE = "linode/debian11"
  24. DEFAULT_REGION = "us-southeast"
  25. )
  26. type GetAllLinodes struct {
  27. Data []GetLinodeResponse `json:"data"`
  28. }
  29. type GetLinodeResponse struct {
  30. Id int `json:"id"`
  31. Ipv4 []string `json:"ipv4"`
  32. Label string `json:"label"`
  33. Created string `json:"created"`
  34. Region string `json:"region"`
  35. Status string `json:"status"`
  36. }
  37. type TypesResponse struct {
  38. Data []TypesResponseInner `json:"data"`
  39. }
  40. type TypesResponseInner struct {
  41. Id string `json:"id"`
  42. }
  43. type ImagesResponse struct {
  44. Data []ImagesResponseInner `json:"data"`
  45. }
  46. type ImagesResponseInner struct {
  47. Id string `json:"id"`
  48. }
  49. type RegionsResponse struct {
  50. Data []RegionResponseInner `json:"data"`
  51. }
  52. type RegionResponseInner struct {
  53. Id string `json:"id"`
  54. }
  55. type NewLinodeBody struct {
  56. Label string `json:"label"`
  57. AuthorizedKeys []string `json:"authorized_keys"`
  58. Booted bool `json:"booted"`
  59. Image string `json:"image"`
  60. RootPass string `json:"root_pass"`
  61. Region string `json:"region"`
  62. Type string `json:"type"`
  63. }
  64. type LinodeConnection struct {
  65. Client *http.Client
  66. Keyring keyring.DaemonKeyRing
  67. KeyTagger keytags.Keytagger
  68. Config *config.Configuration
  69. }
  70. // Logging wrapper
  71. func (ln LinodeConnection) Log(msg ...string) {
  72. lnMsg := []string{"LinodeConnection:"}
  73. lnMsg = append(lnMsg, msg...)
  74. ln.Config.Log(lnMsg...)
  75. }
  76. // Construct a NewLinodeBody struct for a CreateNewLinode call
  77. func NewLinodeBodyBuilder(image string, region string, linodeType string, label string, keyring keyring.DaemonKeyRing) (NewLinodeBody, error) {
  78. var newLnBody NewLinodeBody
  79. rootPass, err := keyring.GetKey(keytags.VPS_ROOT_PASS_KEYNAME)
  80. if err != nil {
  81. return newLnBody, &LinodeClientError{Msg: err.Error()}
  82. }
  83. rootSshKey, err := keyring.GetKey(keytags.SYSTEM_SSH_KEYNAME)
  84. if err != nil {
  85. return newLnBody, &LinodeClientError{Msg: err.Error()}
  86. }
  87. fmt.Println(rootSshKey.GetPublic())
  88. return NewLinodeBody{AuthorizedKeys: []string{rootSshKey.GetPublic()},
  89. Label: label,
  90. RootPass: rootPass.GetSecret(),
  91. Booted: true,
  92. Image: image,
  93. Region: region,
  94. Type: linodeType}, nil
  95. }
  96. /*
  97. Get all regions that a server can be deployed in from Linode
  98. :param keyring: a keyring.DaemonKeyRing implementer that is able to return a linode API key
  99. */
  100. func (ln LinodeConnection) GetRegions() (RegionsResponse, error) {
  101. var regions RegionsResponse
  102. b, err := ln.Get(LinodeRegions)
  103. if err != nil {
  104. return regions, err
  105. }
  106. err = json.Unmarshal(b, &regions)
  107. if err != nil {
  108. return regions, err
  109. }
  110. return regions, nil
  111. }
  112. /*
  113. Get all of the available image types from linode
  114. :param keyring: a keyring.DaemonKeyRing interface implementer. Responsible for getting the linode API key
  115. */
  116. func (ln LinodeConnection) GetImages() (ImagesResponse, error) {
  117. var imgResp ImagesResponse
  118. b, err := ln.Get(LinodeImages)
  119. if err != nil {
  120. return imgResp, err
  121. }
  122. err = json.Unmarshal(b, &imgResp)
  123. if err != nil {
  124. return imgResp, &LinodeClientError{Msg: err.Error()}
  125. }
  126. return imgResp, nil
  127. }
  128. /*
  129. Get all of the available Linode types from linode
  130. :param keyring: a keyring.DaemonKeyRing interface implementer. Responsible for getting the linode API key
  131. */
  132. func (ln LinodeConnection) GetTypes() (TypesResponse, error) {
  133. var typesResp TypesResponse
  134. b, err := ln.Get(LinodeTypes)
  135. if err != nil {
  136. return typesResp, err
  137. }
  138. err = json.Unmarshal(b, &typesResp)
  139. if err != nil {
  140. return typesResp, &LinodeClientError{Msg: err.Error()}
  141. }
  142. return typesResp, nil
  143. }
  144. /*
  145. Get a Linode by its ID, used for assertion when deleting an old linode
  146. */
  147. func (ln LinodeConnection) GetLinode(id string) (GetLinodeResponse, error) {
  148. var getLnResp GetLinodeResponse
  149. b, err := ln.Get(fmt.Sprintf("%s/%s", LinodeInstances, id))
  150. if err != nil {
  151. return getLnResp, err
  152. }
  153. err = json.Unmarshal(b, &getLnResp)
  154. if err != nil {
  155. return getLnResp, &LinodeClientError{Msg: err.Error()}
  156. }
  157. return getLnResp, nil
  158. }
  159. /*
  160. List all linodes on your account
  161. :param keyring: a keyring.DaemonKeyRing implementer that can return the linode API key
  162. */
  163. func (ln LinodeConnection) ListLinodes() (GetAllLinodes, error) {
  164. var allLinodes GetAllLinodes
  165. b, err := ln.Get(LinodeInstances)
  166. if err != nil {
  167. return allLinodes, err
  168. }
  169. err = json.Unmarshal(b, &allLinodes)
  170. if err != nil {
  171. return allLinodes, &LinodeClientError{Msg: err.Error()}
  172. }
  173. return allLinodes, nil
  174. }
  175. /*
  176. Get linode by IP Address
  177. :param addr: the IPv4 address of your linode
  178. */
  179. func (ln LinodeConnection) GetByIp(addr string) (GetLinodeResponse, error) {
  180. var out GetLinodeResponse
  181. servers, err := ln.ListLinodes()
  182. if err != nil {
  183. return out, err
  184. }
  185. for i := range servers.Data {
  186. if servers.Data[i].Ipv4[0] == addr {
  187. return servers.Data[i], nil
  188. }
  189. }
  190. return out, &LinodeClientError{Msg: "Linode with Address of: " + addr + " not found."}
  191. }
  192. /*
  193. Get a linode by its name/label
  194. :param name: the name/label of the linode
  195. */
  196. func (ln LinodeConnection) GetByName(name string) (GetLinodeResponse, error) {
  197. var out GetLinodeResponse
  198. servers, err := ln.ListLinodes()
  199. if err != nil {
  200. return out, err
  201. }
  202. for i := range servers.Data {
  203. if servers.Data[i].Label == name {
  204. return servers.Data[i], nil
  205. }
  206. }
  207. return out, &LinodeClientError{Msg: "Linode with name: " + name + " not found."}
  208. }
  209. /*
  210. Create a new linode instance
  211. :param keyring: a keyring.DaemonKeyRing implementer that can return a linode API key
  212. :param body: the request body for the new linode request
  213. */
  214. func (ln LinodeConnection) CreateNewLinode(body NewLinodeBody) (GetLinodeResponse, error) {
  215. var newLnResp GetLinodeResponse
  216. reqBody, err := json.Marshal(&body)
  217. if err != nil {
  218. return newLnResp, err
  219. }
  220. apiKey, err := ln.Keyring.GetKey(ln.KeyTagger.LinodeApiKeyname())
  221. if err != nil {
  222. return newLnResp, &LinodeClientError{Msg: err.Error()}
  223. }
  224. req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s/%s", LinodeApiUrl, LinodeApiVers, LinodeInstances), bytes.NewReader(reqBody))
  225. req.Header.Add("Authorization", apiKey.Prepare())
  226. req.Header.Add("Content-Type", "application/json")
  227. resp, err := ln.Client.Do(req)
  228. if err != nil {
  229. return newLnResp, err
  230. }
  231. defer resp.Body.Close()
  232. b, err := io.ReadAll(resp.Body)
  233. if err != nil {
  234. return newLnResp, &LinodeClientError{Msg: err.Error()}
  235. }
  236. if resp.StatusCode != 200 {
  237. return newLnResp, &LinodeClientError{Msg: resp.Status + "\n" + string(b)}
  238. }
  239. err = json.Unmarshal(b, &newLnResp)
  240. if err != nil {
  241. return newLnResp, &LinodeClientError{Msg: err.Error()}
  242. }
  243. return newLnResp, nil
  244. }
  245. /*
  246. Delete a linode instance. Internally, this function will check that the linode ID exists before deleting
  247. :param id: the id of the linode.
  248. */
  249. func (ln LinodeConnection) DeleteLinode(id string) error {
  250. _, err := ln.GetLinode(id)
  251. if err != nil {
  252. return &LinodeClientError{Msg: err.Error()}
  253. }
  254. _, err = ln.Delete(fmt.Sprintf("%s/%s", LinodeInstances, id))
  255. if err != nil {
  256. return &LinodeClientError{Msg: err.Error()}
  257. }
  258. return nil
  259. }
  260. /*
  261. Agnostic GET method for calling the upstream linode server
  262. :param keyring: a keyring.DaemonKeyRing implementer to get the linode API key from
  263. :param path: the path to GET, added into the base API url
  264. */
  265. func (ln LinodeConnection) Get(path string) ([]byte, error) {
  266. var b []byte
  267. apiKey, err := ln.Keyring.GetKey(ln.KeyTagger.LinodeApiKeyname())
  268. if err != nil {
  269. return b, &LinodeClientError{Msg: err.Error()}
  270. }
  271. req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s/%s", LinodeApiUrl, LinodeApiVers, strings.TrimPrefix(path, "/")), nil)
  272. if err != nil {
  273. return b, &LinodeClientError{Msg: err.Error()}
  274. }
  275. req.Header.Add("Authorization", apiKey.Prepare())
  276. resp, err := ln.Client.Do(req)
  277. if err != nil {
  278. return b, &LinodeClientError{Msg: err.Error()}
  279. }
  280. defer resp.Body.Close()
  281. b, err = io.ReadAll(resp.Body)
  282. if err != nil {
  283. return b, &LinodeClientError{Msg: err.Error()}
  284. }
  285. return b, nil
  286. }
  287. /*
  288. Agnostic DELETE method for deleting a resource from Linode
  289. :param keyring: a keyring.DaemonKeyRing implementer for getting the linode API key
  290. :param path: the path to perform the DELETE method on
  291. */
  292. func (ln LinodeConnection) Delete(path string) ([]byte, error) {
  293. var b []byte
  294. apiKey, err := ln.Keyring.GetKey(ln.KeyTagger.LinodeApiKeyname())
  295. if err != nil {
  296. return b, &LinodeClientError{Msg: err.Error()}
  297. }
  298. req, err := http.NewRequest("DELETE", fmt.Sprintf("https://%s/%s/%s", LinodeApiUrl, LinodeApiVers, strings.TrimPrefix(path, "/")), nil)
  299. if err != nil {
  300. return b, &LinodeClientError{Msg: err.Error()}
  301. }
  302. req.Header.Add("Authorization", apiKey.Prepare())
  303. resp, err := ln.Client.Do(req)
  304. if err != nil {
  305. return b, &LinodeClientError{Msg: err.Error()}
  306. }
  307. defer resp.Body.Close()
  308. b, err = io.ReadAll(resp.Body)
  309. if err != nil {
  310. return b, &LinodeClientError{Msg: err.Error()}
  311. }
  312. return b, nil
  313. }
  314. /*
  315. Poll for new server creation
  316. :param name: the IPv4 address of the linode server
  317. :param max_tries: the number of calls the client will send to linode before exiting
  318. */
  319. func (ln LinodeConnection) ServerPoll(name string, max_tries int) error {
  320. var count int
  321. for {
  322. count = count + 1
  323. if count > max_tries {
  324. return &LinodeTimeOutError{Tries: max_tries}
  325. }
  326. ln.Log("Polling for server status times: ", fmt.Sprint(count))
  327. resp, err := ln.GetByName(name)
  328. if err != nil {
  329. return err
  330. }
  331. if resp.Status == "running" {
  332. ln.Log("Server: ", resp.Ipv4[0], " showing as: ", resp.Status)
  333. return nil
  334. }
  335. ln.Log("Server inactive, showing status: ", resp.Status)
  336. time.Sleep(time.Second * 3)
  337. }
  338. }
  339. /*
  340. Bootstrap the cloud environment
  341. */
  342. func (ln LinodeConnection) Bootstrap() error { return nil }
  343. /*
  344. ############################################
  345. ########### DAEMON EVENT HANDLERS ##########
  346. ############################################
  347. */
  348. type DeleteLinodeRequest struct {
  349. Name string `json:"name"`
  350. Id string `json:"id"`
  351. }
  352. type AddLinodeRequest struct {
  353. Name string `json:"name"`
  354. Image string `json:"image"`
  355. Region string `json:"region"`
  356. Type string `json:"type"`
  357. }
  358. type PollLinodeRequest struct {
  359. Address string `json:"address"`
  360. }
  361. func (ln LinodeConnection) DeleteLinodeHandler(msg daemonproto.SockMessage) daemonproto.SockMessage {
  362. var req DeleteLinodeRequest
  363. err := json.Unmarshal(msg.Body, &req)
  364. if err != nil {
  365. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_FAILED, []byte(err.Error()))
  366. }
  367. resp, err := ln.GetByName(req.Name)
  368. if err != nil {
  369. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_FAILED, []byte(err.Error()))
  370. }
  371. err = ln.DeleteLinode(fmt.Sprint(resp.Id))
  372. if err != nil {
  373. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_ACCEPTED, []byte(err.Error()))
  374. }
  375. responseMessage := []byte("Server: " + fmt.Sprint(resp.Id) + " was deleted.")
  376. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_OK, responseMessage)
  377. }
  378. /*
  379. Wraps the creation of a linode to make the LinodeRouter function slimmer
  380. :param msg: a daemonproto.SockMessage struct with request info
  381. */
  382. func (ln LinodeConnection) AddLinodeHandler(msg daemonproto.SockMessage) daemonproto.SockMessage {
  383. ln.Log("Recieved request to create a new linode server.")
  384. var payload AddLinodeRequest
  385. err := json.Unmarshal(msg.Body, &payload)
  386. if err != nil {
  387. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_FAILED, []byte(err.Error()))
  388. }
  389. newLinodeReq, err := NewLinodeBodyBuilder(payload.Image,
  390. payload.Region,
  391. payload.Type,
  392. payload.Name,
  393. ln.Keyring)
  394. if err != nil {
  395. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_FAILED, []byte(err.Error()))
  396. }
  397. resp, err := ln.CreateNewLinode(newLinodeReq)
  398. if err != nil {
  399. ln.Log("There was an error creating server: ", payload.Name, err.Error())
  400. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_FAILED, []byte(err.Error()))
  401. }
  402. ln.Log("Server: ", payload.Name, " Created successfully.")
  403. b, _ := json.Marshal(resp)
  404. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_OK, b)
  405. }
  406. /*
  407. Wraps the polling feature of the client in a Handler function
  408. :param msg: a daemonproto.SockMessage that contains the request info
  409. */
  410. func (ln LinodeConnection) PollLinodeHandler(msg daemonproto.SockMessage) daemonproto.SockMessage {
  411. var req PollLinodeRequest
  412. err := json.Unmarshal(msg.Body, &req)
  413. if err != nil {
  414. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_TIMEOUT, []byte(err.Error()))
  415. }
  416. err = ln.ServerPoll(req.Address, 60)
  417. if err != nil {
  418. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_TIMEOUT, []byte(err.Error()))
  419. }
  420. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_OK, []byte("Server is running."))
  421. }
  422. /*
  423. Wraps the show servers functionality in a the correct Route interface
  424. :param msg: a daemonproto.SockMessage that contains a request
  425. */
  426. func (ln LinodeConnection) ShowLinodeHandler(msg daemonproto.SockMessage) daemonproto.SockMessage {
  427. servers, err := ln.ListLinodes()
  428. if err != nil {
  429. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_FAILED, []byte(err.Error()))
  430. }
  431. b, _ := json.Marshal(servers)
  432. return *daemonproto.NewSockMessage(daemonproto.MsgResponse, daemonproto.REQUEST_OK, b)
  433. }
  434. type LinodeRouter struct {
  435. routes map[daemonproto.Method]func(daemonproto.SockMessage) daemonproto.SockMessage
  436. }
  437. func (l *LinodeRouter) Register(method daemonproto.Method, callable func(daemonproto.SockMessage) daemonproto.SockMessage) {
  438. l.routes[method] = callable
  439. }
  440. func (l *LinodeRouter) Routes() map[daemonproto.Method]func(daemonproto.SockMessage) daemonproto.SockMessage {
  441. return l.routes
  442. }
  443. func NewLinodeRouter() *LinodeRouter {
  444. return &LinodeRouter{routes: map[daemonproto.Method]func(daemonproto.SockMessage) daemonproto.SockMessage{}}
  445. }
  446. /*
  447. #####################
  448. ####### ERRORS ######
  449. #####################
  450. */
  451. type LinodeClientError struct {
  452. Msg string
  453. }
  454. func (ln *LinodeClientError) Error() string {
  455. return fmt.Sprintf("There was an error calling linode: '%s'", ln.Msg)
  456. }
  457. type LinodeTimeOutError struct {
  458. Tries int
  459. }
  460. func (ln *LinodeTimeOutError) Error() string {
  461. return "Polling timed out after: " + fmt.Sprint(ln.Tries) + " attempts"
  462. }