client.go 14 KB

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