123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- package linode
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "strings"
- "time"
- "git.aetherial.dev/aeth/yosai/pkg/daemon"
- "git.aetherial.dev/aeth/yosai/pkg/keytags"
- )
- const LinodeApiUrl = "api.linode.com"
- const LinodeInstances = "linode/instances"
- const LinodeImages = "images"
- const LinodeApiVers = "v4"
- const LinodeRegions = "regions"
- const LinodeTypes = "linode/types"
- const (
- DEFAULT_TYPE = "g6-nanode-1"
- DEFAULT_IMAGE = "linode/debian11"
- DEFAULT_REGION = "us-southeast"
- )
- type GetAllLinodes struct {
- Data []GetLinodeResponse `json:"data"`
- }
- type GetLinodeResponse struct {
- Id int `json:"id"`
- Ipv4 []string `json:"ipv4"`
- Label string `json:"label"`
- Created string `json:"created"`
- Region string `json:"region"`
- Status string `json:"status"`
- }
- type TypesResponse struct {
- Data []TypesResponseInner `json:"data"`
- }
- type TypesResponseInner struct {
- Id string `json:"id"`
- }
- type ImagesResponse struct {
- Data []ImagesResponseInner `json:"data"`
- }
- type ImagesResponseInner struct {
- Id string `json:"id"`
- }
- type RegionsResponse struct {
- Data []RegionResponseInner `json:"data"`
- }
- type RegionResponseInner struct {
- Id string `json:"id"`
- }
- type NewLinodeBody struct {
- Label string `json:"label"`
- AuthorizedKeys []string `json:"authorized_keys"`
- Booted bool `json:"booted"`
- Image string `json:"image"`
- RootPass string `json:"root_pass"`
- Region string `json:"region"`
- Type string `json:"type"`
- }
- type LinodeConnection struct {
- Client *http.Client
- Keyring daemon.DaemonKeyRing
- KeyTagger keytags.Keytagger
- Config *daemon.Configuration
- }
- // Logging wrapper
- func (ln LinodeConnection) Log(msg ...string) {
- lnMsg := []string{"LinodeConnection:"}
- lnMsg = append(lnMsg, msg...)
- ln.Config.Log(lnMsg...)
- }
- // Construct a NewLinodeBody struct for a CreateNewLinode call
- func NewLinodeBodyBuilder(image string, region string, linodeType string, label string, keyring daemon.DaemonKeyRing) (NewLinodeBody, error) {
- var newLnBody NewLinodeBody
- rootPass, err := keyring.GetKey(keytags.VPS_ROOT_PASS_KEYNAME)
- if err != nil {
- return newLnBody, &LinodeClientError{Msg: err.Error()}
- }
- rootSshKey, err := keyring.GetKey(keytags.VPS_SSH_KEY_KEYNAME)
- if err != nil {
- return newLnBody, &LinodeClientError{Msg: err.Error()}
- }
- return NewLinodeBody{AuthorizedKeys: []string{rootSshKey.GetPublic()},
- Label: label,
- RootPass: rootPass.GetSecret(),
- Booted: true,
- Image: image,
- Region: region,
- Type: linodeType}, nil
- }
- /*
- Get all regions that a server can be deployed in from Linode
- :param keyring: a daemon.DaemonKeyRing implementer that is able to return a linode API key
- */
- func (ln LinodeConnection) GetRegions() (RegionsResponse, error) {
- var regions RegionsResponse
- b, err := ln.Get(LinodeRegions)
- if err != nil {
- return regions, err
- }
- err = json.Unmarshal(b, ®ions)
- if err != nil {
- return regions, err
- }
- return regions, nil
- }
- /*
- Get all of the available image types from linode
- :param keyring: a daemon.DaemonKeyRing interface implementer. Responsible for getting the linode API key
- */
- func (ln LinodeConnection) GetImages() (ImagesResponse, error) {
- var imgResp ImagesResponse
- b, err := ln.Get(LinodeImages)
- if err != nil {
- return imgResp, err
- }
- err = json.Unmarshal(b, &imgResp)
- if err != nil {
- return imgResp, &LinodeClientError{Msg: err.Error()}
- }
- return imgResp, nil
- }
- /*
- Get all of the available Linode types from linode
- :param keyring: a daemon.DaemonKeyRing interface implementer. Responsible for getting the linode API key
- */
- func (ln LinodeConnection) GetTypes() (TypesResponse, error) {
- var typesResp TypesResponse
- b, err := ln.Get(LinodeTypes)
- if err != nil {
- return typesResp, err
- }
- err = json.Unmarshal(b, &typesResp)
- if err != nil {
- return typesResp, &LinodeClientError{Msg: err.Error()}
- }
- return typesResp, nil
- }
- /*
- Get a Linode by its ID, used for assertion when deleting an old linode
- */
- func (ln LinodeConnection) GetLinode(id string) (GetLinodeResponse, error) {
- var getLnResp GetLinodeResponse
- b, err := ln.Get(fmt.Sprintf("%s/%s", LinodeInstances, id))
- if err != nil {
- return getLnResp, err
- }
- err = json.Unmarshal(b, &getLnResp)
- if err != nil {
- return getLnResp, &LinodeClientError{Msg: err.Error()}
- }
- return getLnResp, nil
- }
- /*
- List all linodes on your account
- :param keyring: a daemon.DaemonKeyRing implementer that can return the linode API key
- */
- func (ln LinodeConnection) ListLinodes() (GetAllLinodes, error) {
- var allLinodes GetAllLinodes
- b, err := ln.Get(LinodeInstances)
- if err != nil {
- return allLinodes, err
- }
- err = json.Unmarshal(b, &allLinodes)
- if err != nil {
- return allLinodes, &LinodeClientError{Msg: err.Error()}
- }
- return allLinodes, nil
- }
- /*
- Get linode by IP Address
- :param addr: the IPv4 address of your linode
- */
- func (ln LinodeConnection) GetByIp(addr string) (GetLinodeResponse, error) {
- var out GetLinodeResponse
- servers, err := ln.ListLinodes()
- if err != nil {
- return out, err
- }
- for i := range servers.Data {
- if servers.Data[i].Ipv4[0] == addr {
- return servers.Data[i], nil
- }
- }
- return out, &LinodeClientError{Msg: "Linode with Address of: " + addr + " not found."}
- }
- /*
- Get a linode by its name/label
- :param name: the name/label of the linode
- */
- func (ln LinodeConnection) GetByName(name string) (GetLinodeResponse, error) {
- var out GetLinodeResponse
- servers, err := ln.ListLinodes()
- if err != nil {
- return out, err
- }
- for i := range servers.Data {
- if servers.Data[i].Label == name {
- return servers.Data[i], nil
- }
- }
- return out, &LinodeClientError{Msg: "Linode with name: " + name + " not found."}
- }
- /*
- Create a new linode instance
- :param keyring: a daemon.DaemonKeyRing implementer that can return a linode API key
- :param body: the request body for the new linode request
- */
- func (ln LinodeConnection) CreateNewLinode(body NewLinodeBody) (GetLinodeResponse, error) {
- var newLnResp GetLinodeResponse
- reqBody, err := json.Marshal(&body)
- if err != nil {
- return newLnResp, err
- }
- apiKey, err := ln.Keyring.GetKey(ln.KeyTagger.LinodeApiKeyname())
- if err != nil {
- return newLnResp, &LinodeClientError{Msg: err.Error()}
- }
- req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s/%s", LinodeApiUrl, LinodeApiVers, LinodeInstances), bytes.NewReader(reqBody))
- req.Header.Add("Authorization", apiKey.Prepare())
- req.Header.Add("Content-Type", "application/json")
- resp, err := ln.Client.Do(req)
- if err != nil {
- return newLnResp, err
- }
- defer resp.Body.Close()
- b, err := io.ReadAll(resp.Body)
- if err != nil {
- return newLnResp, &LinodeClientError{Msg: err.Error()}
- }
- if resp.StatusCode != 200 {
- return newLnResp, &LinodeClientError{Msg: resp.Status + "\n" + string(b)}
- }
- err = json.Unmarshal(b, &newLnResp)
- if err != nil {
- return newLnResp, &LinodeClientError{Msg: err.Error()}
- }
- return newLnResp, nil
- }
- /*
- Delete a linode instance. Internally, this function will check that the linode ID exists before deleting
- :param id: the id of the linode.
- */
- func (ln LinodeConnection) DeleteLinode(id string) error {
- _, err := ln.GetLinode(id)
- if err != nil {
- return &LinodeClientError{Msg: err.Error()}
- }
- _, err = ln.Delete(fmt.Sprintf("%s/%s", LinodeInstances, id))
- if err != nil {
- return &LinodeClientError{Msg: err.Error()}
- }
- return nil
- }
- /*
- Agnostic GET method for calling the upstream linode server
- :param keyring: a daemon.DaemonKeyRing implementer to get the linode API key from
- :param path: the path to GET, added into the base API url
- */
- func (ln LinodeConnection) Get(path string) ([]byte, error) {
- var b []byte
- apiKey, err := ln.Keyring.GetKey(ln.KeyTagger.LinodeApiKeyname())
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s/%s", LinodeApiUrl, LinodeApiVers, strings.TrimPrefix(path, "/")), nil)
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- req.Header.Add("Authorization", apiKey.Prepare())
- resp, err := ln.Client.Do(req)
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- defer resp.Body.Close()
- b, err = io.ReadAll(resp.Body)
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- return b, nil
- }
- /*
- Agnostic DELETE method for deleting a resource from Linode
- :param keyring: a daemon.DaemonKeyRing implementer for getting the linode API key
- :param path: the path to perform the DELETE method on
- */
- func (ln LinodeConnection) Delete(path string) ([]byte, error) {
- var b []byte
- apiKey, err := ln.Keyring.GetKey(ln.KeyTagger.LinodeApiKeyname())
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- req, err := http.NewRequest("DELETE", fmt.Sprintf("https://%s/%s/%s", LinodeApiUrl, LinodeApiVers, strings.TrimPrefix(path, "/")), nil)
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- req.Header.Add("Authorization", apiKey.Prepare())
- resp, err := ln.Client.Do(req)
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- defer resp.Body.Close()
- b, err = io.ReadAll(resp.Body)
- if err != nil {
- return b, &LinodeClientError{Msg: err.Error()}
- }
- return b, nil
- }
- /*
- Poll for new server creation
- :param name: the IPv4 address of the linode server
- :param max_tries: the number of calls the client will send to linode before exiting
- */
- func (ln LinodeConnection) ServerPoll(name string, max_tries int) error {
- var count int
- for {
- count = count + 1
- if count > max_tries {
- return &LinodeTimeOutError{Tries: max_tries}
- }
- ln.Log("Polling for server status times: ", fmt.Sprint(count))
- resp, err := ln.GetByName(name)
- if err != nil {
- return err
- }
- if resp.Status == "running" {
- ln.Log("Server: ", resp.Ipv4[0], " showing as: ", resp.Status)
- return nil
- }
- ln.Log("Server inactive, showing status: ", resp.Status)
- time.Sleep(time.Second * 3)
- }
- }
- /*
- Bootstrap the cloud environment
- */
- func (ln LinodeConnection) Bootstrap() error { return nil }
- /*
- ############################################
- ########### DAEMON EVENT HANDLERS ##########
- ############################################
- */
- type DeleteLinodeRequest struct {
- Name string `json:"name"`
- Id string `json:"id"`
- }
- type AddLinodeRequest struct {
- Name string `json:"name"`
- Image string `json:"image"`
- Region string `json:"region"`
- Type string `json:"type"`
- }
- type PollLinodeRequest struct {
- Address string `json:"address"`
- }
- func (ln LinodeConnection) DeleteLinodeHandler(msg daemon.SockMessage) daemon.SockMessage {
- var req DeleteLinodeRequest
- err := json.Unmarshal(msg.Body, &req)
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_FAILED, []byte(err.Error()))
- }
- resp, err := ln.GetByName(req.Name)
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_FAILED, []byte(err.Error()))
- }
- err = ln.DeleteLinode(fmt.Sprint(resp.Id))
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_ACCEPTED, []byte(err.Error()))
- }
- responseMessage := []byte("Server: " + fmt.Sprint(resp.Id) + " was deleted.")
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_OK, responseMessage)
- }
- /*
- Wraps the creation of a linode to make the LinodeRouter function slimmer
- :param msg: a daemon.SockMessage struct with request info
- */
- func (ln LinodeConnection) AddLinodeHandler(msg daemon.SockMessage) daemon.SockMessage {
- ln.Log("Recieved request to create a new linode server.")
- var payload AddLinodeRequest
- err := json.Unmarshal(msg.Body, &payload)
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_FAILED, []byte(err.Error()))
- }
- newLinodeReq, err := NewLinodeBodyBuilder(payload.Image,
- payload.Region,
- payload.Type,
- payload.Name,
- ln.Keyring)
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_FAILED, []byte(err.Error()))
- }
- resp, err := ln.CreateNewLinode(newLinodeReq)
- if err != nil {
- ln.Log("There was an error creating server: ", payload.Name, err.Error())
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_FAILED, []byte(err.Error()))
- }
- ln.Log("Server: ", payload.Name, " Created successfully.")
- b, _ := json.Marshal(resp)
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_OK, b)
- }
- /*
- Wraps the polling feature of the client in a Handler function
- :param msg: a daemon.SockMessage that contains the request info
- */
- func (ln LinodeConnection) PollLinodeHandler(msg daemon.SockMessage) daemon.SockMessage {
- var req PollLinodeRequest
- err := json.Unmarshal(msg.Body, &req)
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_TIMEOUT, []byte(err.Error()))
- }
- err = ln.ServerPoll(req.Address, 60)
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_TIMEOUT, []byte(err.Error()))
- }
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_OK, []byte("Server is running."))
- }
- /*
- Wraps the show servers functionality in a the correct Route interface
- :param msg: a daemon.SockMessage that contains a request
- */
- func (ln LinodeConnection) ShowLinodeHandler(msg daemon.SockMessage) daemon.SockMessage {
- servers, err := ln.ListLinodes()
- if err != nil {
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_FAILED, []byte(err.Error()))
- }
- b, _ := json.Marshal(servers)
- return *daemon.NewSockMessage(daemon.MsgResponse, daemon.REQUEST_OK, b)
- }
- type LinodeRouter struct {
- routes map[daemon.Method]func(daemon.SockMessage) daemon.SockMessage
- }
- func (l *LinodeRouter) Register(method daemon.Method, callable func(daemon.SockMessage) daemon.SockMessage) {
- l.routes[method] = callable
- }
- func (l *LinodeRouter) Routes() map[daemon.Method]func(daemon.SockMessage) daemon.SockMessage {
- return l.routes
- }
- func NewLinodeRouter() *LinodeRouter {
- return &LinodeRouter{routes: map[daemon.Method]func(daemon.SockMessage) daemon.SockMessage{}}
- }
- /*
- #####################
- ####### ERRORS ######
- #####################
- */
- type LinodeClientError struct {
- Msg string
- }
- func (ln *LinodeClientError) Error() string {
- return fmt.Sprintf("There was an error calling linode: '%s'", ln.Msg)
- }
- type LinodeTimeOutError struct {
- Tries int
- }
- func (ln *LinodeTimeOutError) Error() string {
- return "Polling timed out after: " + fmt.Sprint(ln.Tries) + " attempts"
- }
|