Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add xenorchestra_vdi resource #225

Merged
merged 3 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*.iso filter=lfs diff=lfs merge=lfs -text
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Any and all contributions are welcome! Don't hesitate to reach out to ask if you

- [Terraform](https://www.terraform.io/downloads.html) 0.12+
- [Go](https://golang.org/doc/install) 1.16 (to build the provider plugin)
- git lfs must be installed and `git lfs install` must be run after cloning

## Testing the Provider

Expand All @@ -24,6 +25,7 @@ The following environment variables must be set:
- XOA_TEMPLATE - A VM template that has an existing OS **already installed**
- XOA_DISKLESS_TEMPLATE - A VM template that does not have an existing OS (found from `xe template-list`)
- XOA_ISO - The name of an ISO that exists on the same pool as `XOA_POOL`
- XOA_ISO_SR - The name of an ISO storage repository that exists on the same pool as `XOA_POOL`. This SR must be writable since the tests will upload an ISO to it.
- XOA_NETWORK - The name of a network that is PXE capable. If a non PXE capable network is used some tests may fail.

I typically keep these in a ~/.xoa file and run the following before running the test suite
Expand All @@ -36,7 +38,8 @@ export XOA_USER=username
export XOA_PASSWORD=password
export XOA_POOL=pool-1
export XOA_TEMPLATE='Debian 10 Cloudinit'
export XOA_TEMPLATE='Debian Buster 10'

[ ... ]

# Source the environment variables inside the file
eval $(cat ~/.xoa)
Expand Down
72 changes: 62 additions & 10 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import (
"fmt"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"reflect"
"strings"
"time"

gorillawebsocket "github.com/gorilla/websocket"
Expand Down Expand Up @@ -80,8 +83,12 @@ type XOClient interface {

GetTemplate(template Template) ([]Template, error)

GetAllVDIs() ([]VDI, error)
GetVDIs(vdiReq VDI) ([]VDI, error)
GetVDI(vdiReq VDI) (VDI, error)
CreateVDI(vdiReq CreateVDIReq) (VDI, error)
UpdateVDI(d Disk) error
DeleteVDI(id string) error

CreateAcl(acl Acl) (*Acl, error)
GetAcl(aclReq Acl) (*Acl, error)
Expand Down Expand Up @@ -109,7 +116,9 @@ type XOClient interface {
}

type Client struct {
rpc jsonrpc2.JSONRPC2
rpc jsonrpc2.JSONRPC2
httpClient http.Client
restApiURL *url.URL
}

type Config struct {
Expand All @@ -125,12 +134,12 @@ var dialer = gorillawebsocket.Dialer{
}

func GetConfigFromEnv() Config {
var url string
var wsURL string
var username string
var password string
insecure := false
if v := os.Getenv("XOA_URL"); v != "" {
url = v
wsURL = v
}
if v := os.Getenv("XOA_USER"); v != "" {
username = v
Expand All @@ -142,24 +151,24 @@ func GetConfigFromEnv() Config {
insecure = true
}
return Config{
Url: url,
Url: wsURL,
Username: username,
Password: password,
InsecureSkipVerify: insecure,
}
}

func NewClient(config Config) (XOClient, error) {
url := config.Url
wsURL := config.Url
username := config.Username
password := config.Password
skipVerify := config.InsecureSkipVerify

if skipVerify {
dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
tlsConfig := &tls.Config{
InsecureSkipVerify: config.InsecureSkipVerify,
}
dialer.TLSClientConfig = tlsConfig

ws, _, err := dialer.Dial(fmt.Sprintf("%s/api/", url), http.Header{})
ws, _, err := dialer.Dial(fmt.Sprintf("%s/api/", wsURL), http.Header{})

if err != nil {
return nil, err
Expand All @@ -179,8 +188,41 @@ func NewClient(config Config) (XOClient, error) {
if err != nil {
return nil, err
}

var token string
err = c.Call(context.Background(), "token.create", map[string]interface{}{}, &token)
if err != nil {
return nil, err
}

jar, err := cookiejar.New(&cookiejar.Options{})
if err != nil {
return nil, err
}

restApiURL, err := convertWebsocketURLToRestApi(wsURL)
if err != nil {
return nil, err
}

jar.SetCookies(restApiURL, []*http.Cookie{
&http.Cookie{
Name: "authenticationToken",
Value: token,
MaxAge: 0,
},
})

httpClient := http.Client{
Jar: jar,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
return &Client{
rpc: c,
rpc: c,
httpClient: httpClient,
restApiURL: restApiURL,
}, nil
}

Expand Down Expand Up @@ -306,3 +348,13 @@ type signInResponse struct {
Email string `json:"email,omitempty"`
Id string `json:"id,omitempty"`
}

// This function must be used after a successful JSONRPC websocket request
// is made. This is to verify that we can trust the websocket URL is well-formed
// so our simple ws/wss -> http/https translation will be done correctly
func convertWebsocketURLToRestApi(wsURL string) (*url.URL, error) {
if !strings.HasPrefix(wsURL, "ws") {
return nil, fmt.Errorf("expected `%s` to begin with ws in order to munge the URL to its http/https equivalent\n", wsURL)
}
return url.Parse(strings.Replace(wsURL, "ws", "http", 1))
}
57 changes: 57 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"testing"

"github.com/sourcegraph/jsonrpc2"
Expand Down Expand Up @@ -105,3 +106,59 @@ func TestCall_withNonJsonRPC2Error(t *testing.T) {
t.Errorf("Call method should return an error as is if not of type `jsonrpc2.Error`. Expected: %v received: %v", expectedErr, err)
}
}

func Test_convertWebsocketURLToRestApi(t *testing.T) {
tests := []struct {
inputURL string
expectedURL *url.URL
err error
}{
{
// Use a URL that contains 'ws' more than once to verify
// string replacement only modifies the prefix
inputURL: "ws://example.com/wss",
expectedURL: &url.URL{
Scheme: "http",
Host: "example.com",
Path: "/wss",
},
err: nil,
},
{
// Use a URL that contains 'ws' more than once to verify
// string replacement only modifies the prefix
inputURL: "wss://example.com/wss",
expectedURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/wss",
},
err: nil,
},
{
inputURL: "ftp://example.com",
expectedURL: nil,
err: fmt.Errorf("expected `%s` to begin with ws in order to munge the URL to its http/https equivalent\n", "ftp://example.com"),
},
}

for _, tt := range tests {
url, err := convertWebsocketURLToRestApi(tt.inputURL)

if (tt.err == nil && err != tt.err) || (tt.err != nil && tt.err.Error() != err.Error()) {
t.Errorf("expected error `%v` to match `%v`\n", err, tt.err)
}

if tt.expectedURL == nil && tt.expectedURL != url {
t.Errorf("expected url creation to return nil but instead received `%v`\n", err)
}

if tt.expectedURL != nil {
urlStr := url.String()
expectedURLStr := tt.expectedURL.String()
if expectedURLStr != urlStr {
t.Errorf("expected `%s` and `%s` to match\n", expectedURLStr, urlStr)
}
}
}
}
41 changes: 41 additions & 0 deletions client/storage_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,44 @@ func FindStorageRepositoryForTests(pool Pool, sr *StorageRepository, tag string)
os.Exit(-1)
}
}

func FindIsoStorageRepositoryForTests(pool Pool, sr *StorageRepository, tag, isoSrEnvVar string) {
isoSrName, found := os.LookupEnv(isoSrEnvVar)
if !found {
fmt.Println(fmt.Sprintf("The %s environment variable must be set for the tests", isoSrEnvVar))
os.Exit(-1)
}

c, err := NewClient(GetConfigFromEnv())
if err != nil {
fmt.Printf("failed to create client with error: %v", err)
os.Exit(-1)
}

isoSrReq := StorageRepository{
PoolId: pool.Id,
NameLabel: isoSrName,
SRType: "iso",
}
isoSrs, err := c.GetStorageRepository(isoSrReq)

if err != nil {
fmt.Printf("failed to find an iso storage repository with error: %v\n", err)
os.Exit(-1)
}

if len(isoSrs) != 1 {
fmt.Printf("expected iso srs req `%v` to only return single sr, instead found %d", isoSrReq, len(isoSrs))
os.Exit(-1)
}

isoSr := isoSrs[0]
*sr = isoSr

err = c.AddTag(isoSr.Id, tag)

if err != nil {
fmt.Printf("failed to set tag on iso storage repository with id: %s with error: %v\n", isoSr.Id, err)
os.Exit(-1)
}
ddelnano marked this conversation as resolved.
Show resolved Hide resolved
}
Loading