Merge pull request #4554 from prydie/f-oracle-bmcs
Oracle Bare Metal Cloud Services (BMCS) builder
This commit is contained in:
commit
624b1e5110
|
@ -34,6 +34,7 @@ comes out of the box with support for the following platforms:
|
|||
* Hyper-V
|
||||
* 1&1
|
||||
* OpenStack
|
||||
* Oracle Bare Metal Cloud Services
|
||||
* Parallels
|
||||
* ProfitBricks
|
||||
* QEMU. Both KVM and Xen images.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Copyright (c) 2017 Oracle America, Inc.
|
|
@ -0,0 +1,45 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains a built Custom Image.
|
||||
type Artifact struct {
|
||||
Image client.Image
|
||||
Region string
|
||||
driver Driver
|
||||
}
|
||||
|
||||
// BuilderId uniquely identifies the builder.
|
||||
func (a *Artifact) BuilderId() string {
|
||||
return BuilderId
|
||||
}
|
||||
|
||||
// Files lists the files associated with an artifact. We don't have any files
|
||||
// as the custom image is stored server side.
|
||||
func (a *Artifact) Files() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Id returns the OCID of the associated Image.
|
||||
func (a *Artifact) Id() string {
|
||||
return a.Image.ID
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf(
|
||||
"An image was created: '%v' (OCID: %v) in region '%v'",
|
||||
a.Image.DisplayName, a.Image.ID, a.Region,
|
||||
)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy deletes the custom image associated with the artifact.
|
||||
func (a *Artifact) Destroy() error {
|
||||
return a.driver.DeleteImage(a.Image.ID)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func TestArtifactImpl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Artifact{}
|
||||
if _, ok := raw.(packer.Artifact); !ok {
|
||||
t.Fatalf("Artifact should be artifact")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// Package bmcs contains a packer.Builder implementation that builds Oracle
|
||||
// Bare Metal Cloud Services (BMCS) images.
|
||||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
// BuilderId uniquely identifies the builder
|
||||
const BuilderId = "packer.oracle.bmcs"
|
||||
|
||||
// BMCS API version
|
||||
const bmcsAPIVersion = "20160918"
|
||||
|
||||
// Builder is a builder implementation that creates Oracle BMCS custom images.
|
||||
type Builder struct {
|
||||
config *Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(rawConfig ...interface{}) ([]string, error) {
|
||||
config, err := NewConfig(rawConfig...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.config = config
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
driver, err := NewDriverBMCS(b.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate the state bag
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", b.config)
|
||||
state.Put("driver", driver)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&stepKeyPair{
|
||||
Debug: b.config.PackerDebug,
|
||||
DebugKeyPath: fmt.Sprintf("bmcs_%s.pem", b.config.PackerBuildName),
|
||||
PrivateKeyFile: b.config.Comm.SSHPrivateKey,
|
||||
},
|
||||
&stepCreateInstance{},
|
||||
&stepInstanceInfo{},
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.Comm,
|
||||
Host: commHost,
|
||||
SSHConfig: SSHConfig(
|
||||
b.config.Comm.SSHUsername,
|
||||
b.config.Comm.SSHPassword),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&stepImage{},
|
||||
}
|
||||
|
||||
// Run the steps
|
||||
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &Artifact{
|
||||
Image: state.Get("image").(client.Image),
|
||||
Region: b.config.AccessCfg.Region,
|
||||
driver: driver,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
// Cancel terminates a running build.
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/google/go-querystring/query"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "Content-Type"
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
// baseClient provides a basic (AND INTENTIONALLY INCOMPLETE) JSON REST client
|
||||
// that abstracts away some of the repetitive code required in the BMCS Client.
|
||||
type baseClient struct {
|
||||
httpClient *http.Client
|
||||
method string
|
||||
url string
|
||||
queryStruct interface{}
|
||||
header http.Header
|
||||
body interface{}
|
||||
}
|
||||
|
||||
// newBaseClient constructs a default baseClient.
|
||||
func newBaseClient() *baseClient {
|
||||
return &baseClient{
|
||||
httpClient: http.DefaultClient,
|
||||
method: "GET",
|
||||
header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a copy of an existing baseClient.
|
||||
func (c *baseClient) New() *baseClient {
|
||||
// Copy headers
|
||||
header := make(http.Header)
|
||||
for k, v := range c.header {
|
||||
header[k] = v
|
||||
}
|
||||
|
||||
return &baseClient{
|
||||
httpClient: c.httpClient,
|
||||
method: c.method,
|
||||
url: c.url,
|
||||
header: header,
|
||||
}
|
||||
}
|
||||
|
||||
// Client sets the http Client used to perform requests.
|
||||
func (c *baseClient) Client(httpClient *http.Client) *baseClient {
|
||||
if httpClient == nil {
|
||||
c.httpClient = http.DefaultClient
|
||||
} else {
|
||||
c.httpClient = httpClient
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Base sets the base client url.
|
||||
func (c *baseClient) Base(path string) *baseClient {
|
||||
c.url = path
|
||||
return c
|
||||
}
|
||||
|
||||
// Path extends the client url.
|
||||
func (c *baseClient) Path(path string) *baseClient {
|
||||
baseURL, baseErr := url.Parse(c.url)
|
||||
pathURL, pathErr := url.Parse(path)
|
||||
// Bail on parsing error leaving the client's url unmodified
|
||||
if baseErr != nil || pathErr != nil {
|
||||
return c
|
||||
}
|
||||
|
||||
c.url = baseURL.ResolveReference(pathURL).String()
|
||||
return c
|
||||
}
|
||||
|
||||
// QueryStruct sets the struct from which the request querystring is built.
|
||||
func (c *baseClient) QueryStruct(params interface{}) *baseClient {
|
||||
c.queryStruct = params
|
||||
return c
|
||||
}
|
||||
|
||||
// SetBody wraps a given struct for serialisation and sets the client body.
|
||||
func (c *baseClient) SetBody(params interface{}) *baseClient {
|
||||
c.body = params
|
||||
return c
|
||||
}
|
||||
|
||||
// Header
|
||||
|
||||
// AddHeader adds a HTTP header to the client. Existing keys will be extended.
|
||||
func (c *baseClient) AddHeader(key, value string) *baseClient {
|
||||
c.header.Add(key, value)
|
||||
return c
|
||||
}
|
||||
|
||||
// SetHeader sets a HTTP header on the client. Existing keys will be
|
||||
// overwritten.
|
||||
func (c *baseClient) SetHeader(key, value string) *baseClient {
|
||||
c.header.Add(key, value)
|
||||
return c
|
||||
}
|
||||
|
||||
// HTTP methods (subset)
|
||||
|
||||
// Get sets the client's HTTP method to GET.
|
||||
func (c *baseClient) Get(path string) *baseClient {
|
||||
c.method = "GET"
|
||||
return c.Path(path)
|
||||
}
|
||||
|
||||
// Post sets the client's HTTP method to POST.
|
||||
func (c *baseClient) Post(path string) *baseClient {
|
||||
c.method = "POST"
|
||||
return c.Path(path)
|
||||
}
|
||||
|
||||
// Delete sets the client's HTTP method to DELETE.
|
||||
func (c *baseClient) Delete(path string) *baseClient {
|
||||
c.method = "DELETE"
|
||||
return c.Path(path)
|
||||
}
|
||||
|
||||
// Do executes a HTTP request and returns the response encoded as either error
|
||||
// or success values.
|
||||
func (c *baseClient) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) {
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
if successV != nil {
|
||||
err = json.NewDecoder(resp.Body).Decode(successV)
|
||||
}
|
||||
} else {
|
||||
if failureV != nil {
|
||||
err = json.NewDecoder(resp.Body).Decode(failureV)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Request builds a http.Request from the baseClient instance.
|
||||
func (c *baseClient) Request() (*http.Request, error) {
|
||||
reqURL, err := url.Parse(c.url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.queryStruct != nil {
|
||||
err = addQueryStruct(reqURL, c.queryStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
if c.body != nil {
|
||||
if err := json.NewEncoder(body).Encode(c.body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(c.method, reqURL.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add headers to request
|
||||
for k, vs := range c.header {
|
||||
for _, v := range vs {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Recieve creates a http request from the client and executes it returning the
|
||||
// response.
|
||||
func (c *baseClient) Receive(successV, failureV interface{}) (*http.Response, error) {
|
||||
req, err := c.Request()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Do(req, successV, failureV)
|
||||
}
|
||||
|
||||
// addQueryStruct converts a struct to a querystring and merges any values
|
||||
// provided in the URL itself.
|
||||
func addQueryStruct(reqURL *url.URL, queryStruct interface{}) error {
|
||||
urlValues, err := url.ParseQuery(reqURL.RawQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryValues, err := query.Values(queryStruct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, vs := range queryValues {
|
||||
for _, v := range vs {
|
||||
urlValues.Add(k, v)
|
||||
}
|
||||
}
|
||||
reqURL.RawQuery = urlValues.Encode()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
apiVersion = "20160918"
|
||||
userAgent = "go-bmcs/" + apiVersion
|
||||
baseURLPattern = "https://%s.%s.oraclecloud.com/%s/"
|
||||
)
|
||||
|
||||
// Client is the main interface through which consumers interact with the BMCS
|
||||
// API.
|
||||
type Client struct {
|
||||
UserAgent string
|
||||
Compute *ComputeClient
|
||||
Config *Config
|
||||
}
|
||||
|
||||
// NewClient creates a new Client for communicating with the BMCS API.
|
||||
func NewClient(config *Config) (*Client, error) {
|
||||
transport := NewTransport(http.DefaultTransport, config)
|
||||
base := newBaseClient().Client(&http.Client{Transport: transport})
|
||||
|
||||
return &Client{
|
||||
UserAgent: userAgent,
|
||||
Compute: NewComputeClient(base.New().Base(config.getBaseURL("iaas"))),
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
)
|
||||
|
||||
var (
|
||||
mux *http.ServeMux
|
||||
client *Client
|
||||
server *httptest.Server
|
||||
keyFile *os.File
|
||||
)
|
||||
|
||||
// setup sets up a test HTTP server along with a bmcs.Client that is
|
||||
// configured to talk to that test server. Tests should register handlers on
|
||||
// mux which provide mock responses for the API method being tested.
|
||||
func setup() {
|
||||
mux = http.NewServeMux()
|
||||
server = httptest.NewServer(mux)
|
||||
parsedURL, _ := url.Parse(server.URL)
|
||||
|
||||
config := &Config{}
|
||||
config.baseURL = parsedURL.String()
|
||||
|
||||
var cfg *ini.File
|
||||
var err error
|
||||
cfg, keyFile, err = BaseTestConfig()
|
||||
|
||||
config, err = loadConfigSection(cfg, "DEFAULT", config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client, err = NewClient(config)
|
||||
if err != nil {
|
||||
panic("Failed to instantiate test client")
|
||||
}
|
||||
}
|
||||
|
||||
// teardown closes the test HTTP server
|
||||
func teardown() {
|
||||
server.Close()
|
||||
os.Remove(keyFile.Name())
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package bmcs
|
||||
|
||||
// ComputeClient is a client for the BMCS Compute API.
|
||||
type ComputeClient struct {
|
||||
BaseURL string
|
||||
Instances *InstanceService
|
||||
Images *ImageService
|
||||
VNICAttachments *VNICAttachmentService
|
||||
VNICs *VNICService
|
||||
}
|
||||
|
||||
// NewComputeClient creates a new client for communicating with the BMCS
|
||||
// Compute API.
|
||||
func NewComputeClient(s *baseClient) *ComputeClient {
|
||||
return &ComputeClient{
|
||||
Instances: NewInstanceService(s),
|
||||
Images: NewImageService(s),
|
||||
VNICAttachments: NewVNICAttachmentService(s),
|
||||
VNICs: NewVNICService(s),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// Config API authentication and target configuration
|
||||
type Config struct {
|
||||
// User OCID e.g. ocid1.user.oc1..aaaaaaaadcshyehbkvxl7arse3lv7z5oknexjgfhnhwidtugsxhlm4247
|
||||
User string `ini:"user"`
|
||||
|
||||
// User's Tenancy OCID e.g. ocid1.tenancy.oc1..aaaaaaaagtgvshv6opxzjyzkupkt64ymd32n6kbomadanpcg43d
|
||||
Tenancy string `ini:"tenancy"`
|
||||
|
||||
// Bare metal region identifier (e.g. us-phoenix-1)
|
||||
Region string `ini:"region"`
|
||||
|
||||
// Hex key fingerprint (e.g. b5:a0:62:57:28:0d:fd:c9:59:16:eb:d4:51:9f:70:e4)
|
||||
Fingerprint string `ini:"fingerprint"`
|
||||
|
||||
// Path to BMCS config file (e.g. ~/.oraclebmc/config)
|
||||
KeyFile string `ini:"key_file"`
|
||||
|
||||
// Passphrase used for the key, if it is encrypted.
|
||||
PassPhrase string `ini:"pass_phrase"`
|
||||
|
||||
// Private key (loaded via LoadPrivateKey or ParsePrivateKey)
|
||||
Key *rsa.PrivateKey
|
||||
|
||||
// Used to override base API URL.
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// getBaseURL returns either the specified base URL or builds the appropriate
|
||||
// URL based on service, region, and API version.
|
||||
func (c *Config) getBaseURL(service string) string {
|
||||
if c.baseURL != "" {
|
||||
return c.baseURL
|
||||
}
|
||||
return fmt.Sprintf(baseURLPattern, service, c.Region, apiVersion)
|
||||
}
|
||||
|
||||
// LoadConfigsFromFile loads all oracle bmcs configurations from a file
|
||||
// (generally ~/.oraclebmc/config).
|
||||
func LoadConfigsFromFile(path string) (map[string]*Config, error) {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, fmt.Errorf("Oracle BMCS config file is missing: %s", path)
|
||||
}
|
||||
|
||||
cfgFile, err := ini.Load(path)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Failed to parse config file %s: %s", path, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configs := make(map[string]*Config)
|
||||
|
||||
// Load DEFAULT section to populate defaults for all other configs
|
||||
config, err := loadConfigSection(cfgFile, "DEFAULT", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs["DEFAULT"] = config
|
||||
|
||||
// Load other sections.
|
||||
for _, sectionName := range cfgFile.SectionStrings() {
|
||||
if sectionName == "DEFAULT" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Map to Config struct with defaults from DEFAULT section.
|
||||
config, err := loadConfigSection(cfgFile, sectionName, configs["DEFAULT"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs[sectionName] = config
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// Loads an individual Config object from a ini.Section in the oraclebmc config
|
||||
// file.
|
||||
func loadConfigSection(f *ini.File, sectionName string, config *Config) (*Config, error) {
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
|
||||
section, err := f.GetSection(sectionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Config file does not contain a %s section", sectionName)
|
||||
}
|
||||
|
||||
if err := section.MapTo(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Key, err = LoadPrivateKey(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, err
|
||||
}
|
||||
|
||||
// LoadPrivateKey loads private key from disk and parses it.
|
||||
func LoadPrivateKey(config *Config) (*rsa.PrivateKey, error) {
|
||||
// Expand '~' to $HOME
|
||||
path, err := homedir.Expand(config.KeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and parse API signing key
|
||||
keyContent, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := ParsePrivateKey(keyContent, []byte(config.PassPhrase))
|
||||
|
||||
return key, err
|
||||
}
|
||||
|
||||
// ParsePrivateKey parses a PEM encoded array of bytes into an rsa.PrivateKey.
|
||||
// Attempts to decrypt the PEM encoded array of bytes with the given password
|
||||
// if the PEM encoded byte array is encrypted.
|
||||
func ParsePrivateKey(content, password []byte) (*rsa.PrivateKey, error) {
|
||||
keyBlock, _ := pem.Decode(content)
|
||||
|
||||
if keyBlock == nil {
|
||||
return nil, errors.New("could not decode PEM private key")
|
||||
}
|
||||
|
||||
var der []byte
|
||||
var err error
|
||||
if x509.IsEncryptedPEMBlock(keyBlock) {
|
||||
if len(password) < 1 {
|
||||
return nil, errors.New("encrypted private key but no pass phrase provided")
|
||||
}
|
||||
der, err = x509.DecryptPEMBlock(keyBlock, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
der = keyBlock.Bytes
|
||||
}
|
||||
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS8PrivateKey(der)
|
||||
if err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("Private key is not an RSA private key")
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to parse private key :%s", err)
|
||||
}
|
||||
|
||||
// BaseTestConfig creates the base (DEFAULT) config including a temporary key
|
||||
// file.
|
||||
// NOTE: Caller is responsible for removing temporary key file.
|
||||
func BaseTestConfig() (*ini.File, *os.File, error) {
|
||||
keyFile, err := generateRSAKeyFile()
|
||||
if err != nil {
|
||||
return nil, keyFile, err
|
||||
}
|
||||
// Build ini
|
||||
cfg := ini.Empty()
|
||||
section, _ := cfg.NewSection("DEFAULT")
|
||||
section.NewKey("region", "us-phoenix-1")
|
||||
section.NewKey("tenancy", "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
section.NewKey("user", "ocid1.user.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
section.NewKey("fingerprint", "3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55")
|
||||
section.NewKey("key_file", keyFile.Name())
|
||||
|
||||
return cfg, keyFile, nil
|
||||
}
|
||||
|
||||
// WriteTestConfig writes a ini.File to a temporary file for use in unit tests.
|
||||
// NOTE: Caller is responsible for removing temporary file.
|
||||
func WriteTestConfig(cfg *ini.File) (*os.File, error) {
|
||||
confFile, err := ioutil.TempFile("", "config_file")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = cfg.WriteTo(confFile)
|
||||
if err != nil {
|
||||
os.Remove(confFile.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return confFile, nil
|
||||
}
|
||||
|
||||
// generateRSAKeyFile generates an RSA key file for use in unit tests.
|
||||
// NOTE: The caller is responsible for deleting the temporary file.
|
||||
func generateRSAKeyFile() (*os.File, error) {
|
||||
// Create temporary file for the key
|
||||
f, err := ioutil.TempFile("", "key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate key
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ASN.1 DER encoded form
|
||||
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||
privBlk := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privDer,
|
||||
}
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write(pem.EncodeToMemory(&privBlk)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewConfigMissingFile(t *testing.T) {
|
||||
// WHEN
|
||||
_, err := LoadConfigsFromFile("some/invalid/path")
|
||||
|
||||
// THEN
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected missing file error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigDefaultOnly(t *testing.T) {
|
||||
// GIVEN
|
||||
|
||||
// Get DEFAULT config
|
||||
cfg, keyFile, err := BaseTestConfig()
|
||||
defer os.Remove(keyFile.Name())
|
||||
|
||||
// Write test config to file
|
||||
f, err := WriteTestConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(f.Name()) // clean up
|
||||
|
||||
// WHEN
|
||||
|
||||
// Load configs
|
||||
cfgs, err := LoadConfigsFromFile(f.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// THEN
|
||||
|
||||
if _, ok := cfgs["DEFAULT"]; !ok {
|
||||
t.Fatal("Expected DEFAULT config to exist in map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigDefaultsPopulated(t *testing.T) {
|
||||
// GIVEN
|
||||
|
||||
// Get DEFAULT config
|
||||
cfg, keyFile, err := BaseTestConfig()
|
||||
defer os.Remove(keyFile.Name())
|
||||
|
||||
admin := cfg.Section("ADMIN")
|
||||
admin.NewKey("user", "ocid1.user.oc1..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
admin.NewKey("fingerprint", "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11")
|
||||
|
||||
// Write test config to file
|
||||
f, err := WriteTestConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(f.Name()) // clean up
|
||||
|
||||
// WHEN
|
||||
|
||||
cfgs, err := LoadConfigsFromFile(f.Name())
|
||||
adminConfig, ok := cfgs["ADMIN"]
|
||||
|
||||
// THEN
|
||||
|
||||
if !ok {
|
||||
t.Fatal("Expected ADMIN config to exist in map")
|
||||
}
|
||||
|
||||
if adminConfig.Region != "us-phoenix-1" {
|
||||
t.Errorf("Expected 'us-phoenix-1', got '%s'", adminConfig.Region)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigDefaultsOverridden(t *testing.T) {
|
||||
// GIVEN
|
||||
|
||||
// Get DEFAULT config
|
||||
cfg, keyFile, err := BaseTestConfig()
|
||||
defer os.Remove(keyFile.Name())
|
||||
|
||||
admin := cfg.Section("ADMIN")
|
||||
admin.NewKey("user", "ocid1.user.oc1..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
admin.NewKey("fingerprint", "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11")
|
||||
|
||||
// Write test config to file
|
||||
f, err := WriteTestConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(f.Name()) // clean up
|
||||
|
||||
// WHEN
|
||||
|
||||
cfgs, err := LoadConfigsFromFile(f.Name())
|
||||
adminConfig, ok := cfgs["ADMIN"]
|
||||
|
||||
// THEN
|
||||
|
||||
if !ok {
|
||||
t.Fatal("Expected ADMIN config to exist in map")
|
||||
}
|
||||
|
||||
if adminConfig.Fingerprint != "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11" {
|
||||
t.Errorf("Expected fingerprint '11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11', got '%s'",
|
||||
adminConfig.Fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEncryptedPrivateKeyValidPassword(t *testing.T) {
|
||||
// Generate private key
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected generating RSA key: %+v", err)
|
||||
}
|
||||
publicKey := priv.PublicKey
|
||||
|
||||
// ASN.1 DER encoded form
|
||||
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||
|
||||
blockType := "RSA PRIVATE KEY"
|
||||
password := []byte("password")
|
||||
cipherType := x509.PEMCipherAES256
|
||||
|
||||
// Encrypt priv with password
|
||||
encryptedPEMBlock, err := x509.EncryptPEMBlock(
|
||||
rand.Reader,
|
||||
blockType,
|
||||
privDer,
|
||||
password,
|
||||
cipherType)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error encryting PEM block: %+v", err)
|
||||
}
|
||||
|
||||
// Parse private key
|
||||
key, err := ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), password)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %+v", err)
|
||||
}
|
||||
|
||||
// Check we get the same key back
|
||||
if !reflect.DeepEqual(publicKey, key.PublicKey) {
|
||||
t.Errorf("expected public key of encrypted and decrypted key to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEncryptedPrivateKeyPKCS8(t *testing.T) {
|
||||
// Generate private key
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected generating RSA key: %+v", err)
|
||||
}
|
||||
publicKey := priv.PublicKey
|
||||
|
||||
// Implements x509.MarshalPKCS8PrivateKey which is not included in the
|
||||
// standard library.
|
||||
pkey := struct {
|
||||
Version int
|
||||
PrivateKeyAlgorithm []asn1.ObjectIdentifier
|
||||
PrivateKey []byte
|
||||
}{
|
||||
Version: 0,
|
||||
PrivateKeyAlgorithm: []asn1.ObjectIdentifier{{1, 2, 840, 113549, 1, 1, 1}},
|
||||
PrivateKey: x509.MarshalPKCS1PrivateKey(priv),
|
||||
}
|
||||
privDer, err := asn1.Marshal(pkey)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected marshaling RSA key: %+v", err)
|
||||
}
|
||||
|
||||
blockType := "RSA PRIVATE KEY"
|
||||
password := []byte("password")
|
||||
cipherType := x509.PEMCipherAES256
|
||||
|
||||
// Encrypt priv with password
|
||||
encryptedPEMBlock, err := x509.EncryptPEMBlock(
|
||||
rand.Reader,
|
||||
blockType,
|
||||
privDer,
|
||||
password,
|
||||
cipherType)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error encryting PEM block: %+v", err)
|
||||
}
|
||||
|
||||
// Parse private key
|
||||
key, err := ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), password)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %+v", err)
|
||||
}
|
||||
|
||||
// Check we get the same key back
|
||||
if !reflect.DeepEqual(publicKey, key.PublicKey) {
|
||||
t.Errorf("expected public key of encrypted and decrypted key to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEncryptedPrivateKeyInvalidPassword(t *testing.T) {
|
||||
// Generate private key
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected generating RSA key: %+v", err)
|
||||
}
|
||||
|
||||
// ASN.1 DER encoded form
|
||||
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||
|
||||
blockType := "RSA PRIVATE KEY"
|
||||
password := []byte("password")
|
||||
cipherType := x509.PEMCipherAES256
|
||||
|
||||
// Encrypt priv with password
|
||||
encryptedPEMBlock, err := x509.EncryptPEMBlock(
|
||||
rand.Reader,
|
||||
blockType,
|
||||
privDer,
|
||||
password,
|
||||
cipherType)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error encrypting PEM block: %+v", err)
|
||||
}
|
||||
|
||||
// Parse private key (with wrong password)
|
||||
_, err = ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), []byte("foo"))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "decryption password incorrect") {
|
||||
t.Errorf("Expected error to contain 'decryption password incorrect', got %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEncryptedPrivateKeyInvalidNoPassword(t *testing.T) {
|
||||
// Generate private key
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected generating RSA key: %+v", err)
|
||||
}
|
||||
|
||||
// ASN.1 DER encoded form
|
||||
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||
|
||||
blockType := "RSA PRIVATE KEY"
|
||||
password := []byte("password")
|
||||
cipherType := x509.PEMCipherAES256
|
||||
|
||||
// Encrypt priv with password
|
||||
encryptedPEMBlock, err := x509.EncryptPEMBlock(
|
||||
rand.Reader,
|
||||
blockType,
|
||||
privDer,
|
||||
password,
|
||||
cipherType)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error encrypting PEM block: %+v", err)
|
||||
}
|
||||
|
||||
// Parse private key (with wrong password)
|
||||
_, err = ParsePrivateKey(pem.EncodeToMemory(encryptedPEMBlock), []byte{})
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no pass phrase provided") {
|
||||
t.Errorf("Expected error to contain 'no pass phrase provided', got %+v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package bmcs
|
||||
|
||||
import "fmt"
|
||||
|
||||
// APIError encapsulates an error returned from the API
|
||||
type APIError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e APIError) Error() string {
|
||||
return fmt.Sprintf("BMCS: [%s] '%s'", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// firstError is a helper function to work out which error to return from calls
|
||||
// to the API.
|
||||
func firstError(err error, apiError *APIError) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if apiError != nil && len(apiError.Code) > 0 {
|
||||
return apiError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ImageService enables communicating with the BMCS compute API's instance
|
||||
// related endpoints.
|
||||
type ImageService struct {
|
||||
client *baseClient
|
||||
}
|
||||
|
||||
// NewImageService creates a new ImageService for communicating with the
|
||||
// BMCS compute API's instance related endpoints.
|
||||
func NewImageService(s *baseClient) *ImageService {
|
||||
return &ImageService{
|
||||
client: s.New().Path("images/"),
|
||||
}
|
||||
}
|
||||
|
||||
// Image details a BMCS boot disk image.
|
||||
type Image struct {
|
||||
// The OCID of the image originally used to launch the instance.
|
||||
BaseImageID string `json:"baseImageId,omitempty"`
|
||||
|
||||
// The OCID of the compartment containing the instance you want to use
|
||||
// as the basis for the image.
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
|
||||
// Whether instances launched with this image can be used to create new
|
||||
// images.
|
||||
CreateImageAllowed bool `json:"createImageAllowed"`
|
||||
|
||||
// A user-friendly name for the image. It does not have to be unique,
|
||||
// and it's changeable. You cannot use an Oracle-provided image name
|
||||
// as a custom image name.
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
|
||||
// The OCID of the image.
|
||||
ID string `json:"id"`
|
||||
|
||||
// Current state of the image. Allowed values are:
|
||||
// - PROVISIONING
|
||||
// - AVAILABLE
|
||||
// - DISABLED
|
||||
// - DELETED
|
||||
LifecycleState string `json:"lifecycleState"`
|
||||
|
||||
// The image's operating system (e.g. Oracle Linux).
|
||||
OperatingSystem string `json:"operatingSystem"`
|
||||
|
||||
// The image's operating system version (e.g. 7.2).
|
||||
OperatingSystemVersion string `json:"operatingSystemVersion"`
|
||||
|
||||
// The date and time the image was created.
|
||||
TimeCreated time.Time `json:"timeCreated"`
|
||||
}
|
||||
|
||||
// GetImageParams are the paramaters available when communicating with the
|
||||
// GetImage API endpoint.
|
||||
type GetImageParams struct {
|
||||
ID string `url:"imageId"`
|
||||
}
|
||||
|
||||
// Get returns a single Image
|
||||
func (s *ImageService) Get(params *GetImageParams) (Image, error) {
|
||||
image := Image{}
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Get(params.ID).Receive(&image, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return image, err
|
||||
}
|
||||
|
||||
// CreateImageParams are the parameters available when communicating with
|
||||
// the CreateImage API endpoint.
|
||||
type CreateImageParams struct {
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
InstanceID string `json:"instanceId"`
|
||||
}
|
||||
|
||||
// Create creates a new custom image based on a running compute instance. It
|
||||
// does *not* wait for the imaging process to finish.
|
||||
func (s *ImageService) Create(params *CreateImageParams) (Image, error) {
|
||||
image := Image{}
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Post("").SetBody(params).Receive(&image, &e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return image, err
|
||||
}
|
||||
|
||||
// GetResourceState GETs the LifecycleState of the given image id.
|
||||
func (s *ImageService) GetResourceState(id string) (string, error) {
|
||||
image, err := s.Get(&GetImageParams{ID: id})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return image.LifecycleState, nil
|
||||
|
||||
}
|
||||
|
||||
// DeleteImageParams are the parameters available when communicating with
|
||||
// the DeleteImage API endpoint.
|
||||
type DeleteImageParams struct {
|
||||
ID string `url:"imageId"`
|
||||
}
|
||||
|
||||
// Delete deletes an existing custom image.
|
||||
// NOTE: Deleting an image results in the API endpoint returning 404 on
|
||||
// subsequent calls. As such deletion can't be waited on with a Waiter.
|
||||
func (s *ImageService) Delete(params *DeleteImageParams) error {
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Delete(params.ID).SetBody(params).Receive(nil, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetImage(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.image.oc1.phx.a"
|
||||
path := fmt.Sprintf("/images/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"id":"%s"}`, id)
|
||||
})
|
||||
|
||||
image, err := client.Compute.Images.Get(&GetImageParams{ID: id})
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Images.Get() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := Image{ID: id}
|
||||
|
||||
if !reflect.DeepEqual(image, want) {
|
||||
t.Errorf("Client.Compute.Images.Get() returned %+v, want %+v", image, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateImage(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/images/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"displayName": "go-bmcs test"}`)
|
||||
})
|
||||
|
||||
params := &CreateImageParams{
|
||||
CompartmentID: "ocid1.compartment.oc1..a",
|
||||
DisplayName: "go-bmcs test image",
|
||||
InstanceID: "ocid1.image.oc1.phx.a",
|
||||
}
|
||||
|
||||
image, err := client.Compute.Images.Create(params)
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Images.Create() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := Image{DisplayName: "go-bmcs test"}
|
||||
|
||||
if !reflect.DeepEqual(image, want) {
|
||||
t.Errorf("Client.Compute.Images.Create() returned %+v, want %+v", image, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGetResourceState(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.image.oc1.phx.a"
|
||||
path := fmt.Sprintf("/images/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"LifecycleState": "AVAILABLE"}`)
|
||||
})
|
||||
|
||||
state, err := client.Compute.Images.GetResourceState(id)
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Images.GetResourceState() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := "AVAILABLE"
|
||||
if state != want {
|
||||
t.Errorf("Client.Compute.Images.GetResourceState() returned %+v, want %+v", state, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageGetResourceStateInvalidID(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.image.oc1.phx.a"
|
||||
path := fmt.Sprintf("/images/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"code": "NotAuthorizedOrNotFound"}`)
|
||||
})
|
||||
|
||||
state, err := client.Compute.Images.GetResourceState(id)
|
||||
if err == nil {
|
||||
t.Errorf("Client.Compute.Images.GetResourceState() expected error, got %v", state)
|
||||
}
|
||||
|
||||
want := &APIError{Code: "NotAuthorizedOrNotFound"}
|
||||
if !reflect.DeepEqual(err, want) {
|
||||
t.Errorf("Client.Compute.Images.GetResourceState() errored with %+v, want %+v", err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteInstance(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.image.oc1.phx.a"
|
||||
path := fmt.Sprintf("/images/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
err := client.Compute.Images.Delete(&DeleteImageParams{ID: id})
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Images.Delete() returned error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// InstanceService enables communicating with the BMCS compute API's instance
|
||||
// related endpoints.
|
||||
type InstanceService struct {
|
||||
client *baseClient
|
||||
}
|
||||
|
||||
// NewInstanceService creates a new InstanceService for communicating with the
|
||||
// BMCS compute API's instance related endpoints.
|
||||
func NewInstanceService(s *baseClient) *InstanceService {
|
||||
return &InstanceService{
|
||||
client: s.New().Path("instances/"),
|
||||
}
|
||||
}
|
||||
|
||||
// Instance details a BMCS compute instance.
|
||||
type Instance struct {
|
||||
// The Availability Domain the instance is running in.
|
||||
AvailabilityDomain string `json:"availabilityDomain"`
|
||||
|
||||
// The OCID of the compartment that contains the instance.
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
|
||||
// A user-friendly name. Does not have to be unique, and it's changeable.
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
|
||||
// The OCID of the instance.
|
||||
ID string `json:"id"`
|
||||
|
||||
// The image used to boot the instance.
|
||||
ImageID string `json:"imageId,omitempty"`
|
||||
|
||||
// The current state of the instance. Allowed values:
|
||||
// - PROVISIONING
|
||||
// - RUNNING
|
||||
// - STARTING
|
||||
// - STOPPING
|
||||
// - STOPPED
|
||||
// - CREATING_IMAGE
|
||||
// - TERMINATING
|
||||
// - TERMINATED
|
||||
LifecycleState string `json:"lifecycleState"`
|
||||
|
||||
// Custom metadata that you provide.
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
|
||||
// The region that contains the Availability Domain the instance is running in.
|
||||
Region string `json:"region"`
|
||||
|
||||
// The shape of the instance. The shape determines the number of CPUs
|
||||
// and the amount of memory allocated to the instance.
|
||||
Shape string `json:"shape"`
|
||||
|
||||
// The date and time the instance was created.
|
||||
TimeCreated time.Time `json:"timeCreated"`
|
||||
}
|
||||
|
||||
// GetInstanceParams are the paramaters available when communicating with the
|
||||
// GetInstance API endpoint.
|
||||
type GetInstanceParams struct {
|
||||
ID string `url:"instanceId,omitempty"`
|
||||
}
|
||||
|
||||
// Get returns a single Instance
|
||||
func (s *InstanceService) Get(params *GetInstanceParams) (Instance, error) {
|
||||
instance := Instance{}
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Get(params.ID).Receive(&instance, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return instance, err
|
||||
}
|
||||
|
||||
// LaunchInstanceParams are the parameters available when communicating with
|
||||
// the LunchInstance API endpoint.
|
||||
type LaunchInstanceParams struct {
|
||||
AvailabilityDomain string `json:"availabilityDomain,omitempty"`
|
||||
CompartmentID string `json:"compartmentId,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
ImageID string `json:"imageId,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
OPCiPXEScript string `json:"opcIpxeScript,omitempty"`
|
||||
Shape string `json:"shape,omitempty"`
|
||||
SubnetID string `json:"subnetId,omitempty"`
|
||||
}
|
||||
|
||||
// Launch creates a new BMCS compute instance. It does *not* wait for the
|
||||
// instance to boot.
|
||||
func (s *InstanceService) Launch(params *LaunchInstanceParams) (Instance, error) {
|
||||
instance := &Instance{}
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Post("").SetBody(params).Receive(instance, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return *instance, err
|
||||
}
|
||||
|
||||
// TerminateInstanceParams are the parameters available when communicating with
|
||||
// the TerminateInstance API endpoint.
|
||||
type TerminateInstanceParams struct {
|
||||
ID string `url:"instanceId,omitempty"`
|
||||
}
|
||||
|
||||
// Terminate terminates a running BMCS compute instance.
|
||||
// instance to boot.
|
||||
func (s *InstanceService) Terminate(params *TerminateInstanceParams) error {
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Delete(params.ID).SetBody(params).Receive(nil, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetResourceState GETs the LifecycleState of the given instance id.
|
||||
func (s *InstanceService) GetResourceState(id string) (string, error) {
|
||||
instance, err := s.Get(&GetInstanceParams{ID: id})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return instance.LifecycleState, nil
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetInstance(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.instance.oc1.phx.a"
|
||||
path := fmt.Sprintf("/instances/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"id":"%s"}`, id)
|
||||
})
|
||||
|
||||
instance, err := client.Compute.Instances.Get(&GetInstanceParams{ID: id})
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Instances.Get() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := Instance{ID: id}
|
||||
|
||||
if !reflect.DeepEqual(instance, want) {
|
||||
t.Errorf("Client.Compute.Instances.Get() returned %+v, want %+v", instance, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchInstance(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/instances/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"displayName": "go-bmcs test"}`)
|
||||
})
|
||||
|
||||
params := &LaunchInstanceParams{
|
||||
AvailabilityDomain: "aaaa:PHX-AD-1",
|
||||
CompartmentID: "ocid1.compartment.oc1..a",
|
||||
DisplayName: "go-bmcs test",
|
||||
ImageID: "ocid1.image.oc1.phx.a",
|
||||
Shape: "VM.Standard1.1",
|
||||
SubnetID: "ocid1.subnet.oc1.phx.a",
|
||||
}
|
||||
|
||||
instance, err := client.Compute.Instances.Launch(params)
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Instances.Launch() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := Instance{DisplayName: "go-bmcs test"}
|
||||
|
||||
if !reflect.DeepEqual(instance, want) {
|
||||
t.Errorf("Client.Compute.Instances.Launch() returned %+v, want %+v", instance, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminateInstance(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.instance.oc1.phx.a"
|
||||
path := fmt.Sprintf("/instances/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
err := client.Compute.Instances.Terminate(&TerminateInstanceParams{ID: id})
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Instances.Terminate() returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceGetResourceState(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.instance.oc1.phx.a"
|
||||
path := fmt.Sprintf("/instances/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"LifecycleState": "RUNNING"}`)
|
||||
})
|
||||
|
||||
state, err := client.Compute.Instances.GetResourceState(id)
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.Instances.GetResourceState() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := "RUNNING"
|
||||
if state != want {
|
||||
t.Errorf("Client.Compute.Instances.GetResourceState() returned %+v, want %+v", state, want)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type nopCloser struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transport adds BMCS signature authentication to each outgoing request.
|
||||
type Transport struct {
|
||||
transport http.RoundTripper
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewTransport creates a new Transport to add BMCS signature authentication
|
||||
// to each outgoing request.
|
||||
func NewTransport(transport http.RoundTripper, config *Config) *Transport {
|
||||
return &Transport{
|
||||
transport: transport,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var buf *bytes.Buffer
|
||||
|
||||
if req.Body != nil {
|
||||
buf = new(bytes.Buffer)
|
||||
buf.ReadFrom(req.Body)
|
||||
req.Body = nopCloser{buf}
|
||||
}
|
||||
if req.Header.Get("date") == "" {
|
||||
req.Header.Set("date", time.Now().UTC().Format(http.TimeFormat))
|
||||
}
|
||||
if req.Header.Get("content-type") == "" {
|
||||
req.Header.Set("content-type", "application/json")
|
||||
}
|
||||
if req.Header.Get("accept") == "" {
|
||||
req.Header.Set("accept", "application/json")
|
||||
}
|
||||
if req.Header.Get("host") == "" {
|
||||
req.Header.Set("host", req.URL.Host)
|
||||
}
|
||||
|
||||
var signheaders []string
|
||||
if (req.Method == "PUT" || req.Method == "POST") && buf != nil {
|
||||
signheaders = []string{"(request-target)", "host", "date",
|
||||
"content-length", "content-type", "x-content-sha256"}
|
||||
|
||||
if req.Header.Get("content-length") == "" {
|
||||
req.Header.Set("content-length", strconv.Itoa(buf.Len()))
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write(buf.Bytes())
|
||||
hash := hasher.Sum(nil)
|
||||
req.Header.Set("x-content-sha256", base64.StdEncoding.EncodeToString(hash))
|
||||
} else {
|
||||
signheaders = []string{"date", "host", "(request-target)"}
|
||||
}
|
||||
|
||||
var signbuffer bytes.Buffer
|
||||
for idx, header := range signheaders {
|
||||
signbuffer.WriteString(header)
|
||||
signbuffer.WriteString(": ")
|
||||
|
||||
if header == "(request-target)" {
|
||||
signbuffer.WriteString(strings.ToLower(req.Method))
|
||||
signbuffer.WriteString(" ")
|
||||
signbuffer.WriteString(req.URL.RequestURI())
|
||||
} else {
|
||||
signbuffer.WriteString(req.Header.Get(header))
|
||||
}
|
||||
|
||||
if idx < len(signheaders)-1 {
|
||||
signbuffer.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write(signbuffer.Bytes())
|
||||
digest := h.Sum(nil)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, t.config.Key, crypto.SHA256, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authHeader := fmt.Sprintf("Signature headers=\"%s\","+
|
||||
"keyId=\"%s/%s/%s\","+
|
||||
"algorithm=\"rsa-sha256\","+
|
||||
"signature=\"%s\","+
|
||||
"version=\"1\"",
|
||||
strings.Join(signheaders, " "),
|
||||
t.config.Tenancy, t.config.User, t.config.Fingerprint,
|
||||
base64.StdEncoding.EncodeToString(signature))
|
||||
req.Header.Add("Authorization", authHeader)
|
||||
|
||||
return t.transport.RoundTrip(req)
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAyLnyzmYj0dZuDo2nclIdEyLZrFZLtw5xFldWpCUl5W3SxKDL
|
||||
iIgwxpSO75Yf++Rzqc5j6S5bpIrdca6AwVXCNmjjxMPO7vLLm4l4IUOZMv5FqKaC
|
||||
I2plFz9uBkzGscnYnMbsDA430082E07lYpNv1xy8JwpbrIsqIMh4XCKci/Od5sLR
|
||||
kEicmOpQK42FGRTQjjmQoWtv+9XED+vYTRL0AxQEC/6i/E7yssFXZ+fpHSKWeKTQ
|
||||
K/1Fc4pZ1zNzJcDXGuweISx/QMLz78TAPH5OBq/EQzHKSpKvfnWFRyBHne8pwuN8
|
||||
8wzbbD+/7OFjz28jNSERVJvfYe3X1k69IWMNGwIDAQABAoIBAQCZhcdU38AzxSrG
|
||||
DMfuYymDslsEObiNWQlbig9lWlhCwx26cDVbxrZvm747NvpdgVyJmqbF+UP0dJVs
|
||||
Voh51qrFTLIwk4bZMXBTFPCBmJ865knG9RuCFOUew8/WF7C82GHJf0eY7OL7xpDY
|
||||
cbZ2D8gxofOydHSrYoElM88CwSI00xPCbBKEMrBO94oXC8yfp2bmV6bNhVXwFDEM
|
||||
qda7M6jVAsBrTOzxUF5VdUUU/MLsu2cCk/ap1zer2Bml9Afk1bMeGJ3XDQgol0pS
|
||||
CLxaGczpSNVMF9+pjA5sFHR5rmcl0b/kC9HsgOJGhLOimtS94O64dSdWifgsjf6+
|
||||
fhT2SMiRAoGBAOUDwkdzNqQfvS+qrP2ULpB4vt7MZ70rDGmyMDLZ1VWgcm2cDIad
|
||||
b7MkFG6GCa48mKkFXK9mOPyq8ELoTjZo2p+relEqf49BpaNxr+cp11pX7g0AkzCa
|
||||
a8LwdOOUW/poqYl2xLuw9Rz6ky6ybzatMvCwpQCwnbAdABIVxz4oQKHpAoGBAOBg
|
||||
3uYC/ynGdF9gJTfdS5XPYoLcKKRRspBZrvvDHaWyBnanm5KYwDAZPzLQMqcpyPeo
|
||||
5xgwMmtNlc6lKKyGkhSLNCV+eO3yAx1h/cq7ityvMS7u6o5sq+/bvtEnbUPYbEtk
|
||||
AhVD7/w5Yyzzi4beiQxDKe0q1mvUAH56aGqJivBjAoGBALmUMTPbDhUzTwg4Y1Rd
|
||||
ZtpVrj43H31wS+++oEYktTZc/T0LLi9Llr9w5kmlvmR94CtfF/ted6FwF5/wRajb
|
||||
kQXAXC83pAR/au0mbCeDhWpFRLculxfUmqxuVBozF9G0TGYDY2rA+++OsgQuPebt
|
||||
tRDL4/nKJQ4Ygf0lvr4EulM5AoGBALoIdyabu3WmfhwJujH8P8wA+ztmUCgVOIio
|
||||
YwWIe49C8Er2om1ESqxWcmit6CFi6qY0Gw6Z/2OqGxgPJY8NsBZqaBziJF+cdWqq
|
||||
MWMiZXqdopi4LC9T+KZROn9tQhGrYfaL/5IkFti3t/uwHbH/1f8dvKhQCSGzz4kN
|
||||
8n7KdTDjAoGAKh8XdIOQlThFK108VT2yp4BGZXEOvWrR19DLbuUzHVrEX+Bd+uFo
|
||||
Ruk9iKEH7PSnlRnIvWc1y9qN7fUi5OR3LvQNGlXHyUl6CtmH3/b064bBKudC+XTn
|
||||
VBelcIoGpH7Dn9I6pKUFauJz1TSbQCIjYGNqrjyzLtG+lH/gy5q4xs8=
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
type testTarget struct {
|
||||
CapturedReq *http.Request
|
||||
Response *http.Response
|
||||
}
|
||||
|
||||
func (target *testTarget) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
target.CapturedReq = req
|
||||
return target.Response, nil
|
||||
}
|
||||
|
||||
func testReq(method string, url string, body string, headers map[string]string) *http.Request {
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
var KnownGoodCases = []struct {
|
||||
name string
|
||||
inputRequest func() *http.Request
|
||||
expectedHeaders map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Simple GET",
|
||||
inputRequest: func() *http.Request {
|
||||
return testReq("GET", "https://example.com/", "", map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
|
||||
},
|
||||
expectedHeaders: map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
|
||||
"content-type": "application/json",
|
||||
"host": "example.com",
|
||||
"accept": "application/json",
|
||||
"Authorization": "Signature headers=\"date host (request-target)\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"UMw/FImQYZ5JBpfYcR9YN72lhupGl5yS+522NS9glLodU9f4oKRqaocpGdSUSRhhSDKxIx01rV547/HemJ6QqEPaJJuDQPXsGthokWMU2DBGyaMAqhLClgCJiRQMwpg4rdL2tETzkM3wy6UN+I52RYoNSdsnat2ZArCkfl8dIl9ydcwD8/+BqB8d2wyaAIS4iagdPKLAC/Mu9OzyUPOXQhYGYsoEdOowOUkHOlob65PFrlHmKJDdjEF3MDcygEpApItf4iUEloP5bjixAbZEVpj3HLQ5uaPx9m+RsLzYMeO0adE0wOv2YNmwZrExGhXh1BpTU33m5amHeUBxSaG+2A==\",version=\"1\"",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Simple PUT request",
|
||||
inputRequest: func() *http.Request {
|
||||
return testReq("PUT", "https://example.com/", "Some Content", map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
|
||||
},
|
||||
expectedHeaders: map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
|
||||
"content-type": "application/json",
|
||||
"content-length": "12",
|
||||
"x-content-sha256": "lQ8fsURxamLtHxnwTYqd3MNYadJ4ZB/U9yQBKzu/fXA=",
|
||||
"accept": "application/json",
|
||||
"Authorization": "Signature headers=\"(request-target) host date content-length content-type x-content-sha256\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"FHyPt4PE2HGH+iftzcViB76pCJ2R9+DdTCo1Ss4mH4KHQJdyQtPsCpe6Dc19zie6cRr6dsenk21yYnncic8OwZhII8DULj2//qLFGmgFi84s7LJqMQ/COiP7O9KtCN+U8MMt4PV7ZDsiGFn3/8EUJ1wxYscxSIB19S1NpuEL062JgGfkqxTkTPd7V3Xh1NlmUUtQrAMR3l56k1iV0zXY9Uw0CjWYjueMP0JUmkO7zycYAVBrx7Q8wkmejlyD7yFrAnObyEsMm9cIL9IcruWFHeCHFxRLslw7AoLxibAm2Dc9EROuvCK2UkUp8AFkE+QyYDMrrSm1NLBMWdnYqdickA==\",version=\"1\"",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Simple POST request",
|
||||
inputRequest: func() *http.Request {
|
||||
return testReq("POST", "https://example.com/", "Some Content", map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
|
||||
},
|
||||
expectedHeaders: map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
|
||||
"content-type": "application/json",
|
||||
"content-length": "12",
|
||||
"x-content-sha256": "lQ8fsURxamLtHxnwTYqd3MNYadJ4ZB/U9yQBKzu/fXA=",
|
||||
"accept": "application/json",
|
||||
"Authorization": "Signature headers=\"(request-target) host date content-length content-type x-content-sha256\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"WzGIoySkjqydwabMTxjVs05UBu0hThAEBzVs7HbYO45o2XpaoqGiNX67mNzs1PeYrGHpJp8+Ysoz66PChWV/1trxuTU92dQ/FgwvcwBRy5dQvdLkjWCZihNunSk4gt9652w6zZg/ybLon0CFbLRnlanDJDX9BgR3ttuTxf30t5qr2A4fnjFF4VjaU/CzE13cNfaWftjSd+xNcla2sbArF3R0+CEEb0xZEPzTyjjjkyvXdaPZwEprVn8IDmdJvLmRP4EniAPxE1EZIhd712M5ondQkR4/WckM44/hlKDeXGFb4y+QnU02i4IWgOWs3dh2tuzS1hp1zfq7qgPbZ4hp0A==\",version=\"1\"",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Simple DELETE",
|
||||
inputRequest: func() *http.Request {
|
||||
return testReq("DELETE", "https://example.com/", "Some Content", map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT"})
|
||||
},
|
||||
expectedHeaders: map[string]string{
|
||||
"date": "Mon, 26 Sep 2016 11:04:22 GMT",
|
||||
"content-type": "application/json",
|
||||
"accept": "application/json",
|
||||
"Authorization": "Signature headers=\"date host (request-target)\",keyId=\"tenant/testuser/3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55\",algorithm=\"rsa-sha256\",signature=\"Kj4YSpONZG1cibLbNgxIp4VoS5+80fsB2Fh2Ue28+QyXq4wwrJpMP+8jEupz1yTk1SNPYuxsk7lNOgtI6G1Hq0YJJVum74j46sUwRWe+f08tMJ3c9J+rrzLfpIrakQ8PaudLhHU0eK5kuTZme1dCwRWXvZq3r5IqkGot/OGMabKpBygRv9t0i5ry+bTslSjMqafTWLosY9hgIiGrXD+meB5tpyn+gPVYc//Hc/C7uNNgLJIMk5DKVa4U0YnoY3ojafZTXZQQNGRn2NDMcZUX3f3nJlUIfiZRiOCTkbPwx/fWb4MZtYaEsY5OPficbJRvfOBxSG1wjX+8rgO7ijhMAA==\",version=\"1\"",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestKnownGoodRequests(t *testing.T) {
|
||||
pKey, err := ParsePrivateKey([]byte(testKey), []byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse test key: %s", err.Error())
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Key: pKey,
|
||||
User: "testuser",
|
||||
Tenancy: "tenant",
|
||||
Fingerprint: "3c:b6:44:d7:49:1a:ac:bf:de:7d:76:22:a7:f5:df:55",
|
||||
}
|
||||
|
||||
expectedResponse := &http.Response{}
|
||||
for _, tt := range KnownGoodCases {
|
||||
targetBackend := &testTarget{Response: expectedResponse}
|
||||
target := NewTransport(targetBackend, config)
|
||||
|
||||
_, err = target.RoundTrip(tt.inputRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Failed to handle request %s", tt.name, err.Error())
|
||||
}
|
||||
|
||||
sentReq := targetBackend.CapturedReq
|
||||
|
||||
for header, val := range tt.expectedHeaders {
|
||||
if sentReq.Header.Get(header) != val {
|
||||
t.Fatalf("%s: Header mismatch in responnse,\n\t expecting \"%s\"\n\t got \"%s\"", tt.name, val, sentReq.Header.Get(header))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// VNICService enables communicating with the BMCS compute API's VNICs
|
||||
// endpoint.
|
||||
type VNICService struct {
|
||||
client *baseClient
|
||||
}
|
||||
|
||||
// NewVNICService creates a new VNICService for communicating with the
|
||||
// BMCS compute API's instance related endpoints.
|
||||
func NewVNICService(s *baseClient) *VNICService {
|
||||
return &VNICService{client: s.New().Path("vnics/")}
|
||||
}
|
||||
|
||||
// VNIC - a virtual network interface card.
|
||||
type VNIC struct {
|
||||
AvailabilityDomain string `json:"availabilityDomain"`
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
ID string `json:"id"`
|
||||
LifecycleState string `json:"lifecycleState"`
|
||||
PrivateIP string `json:"privateIp"`
|
||||
PublicIP string `json:"publicIp"`
|
||||
SubnetID string `json:"subnetId"`
|
||||
TimeCreated time.Time `json:"timeCreated"`
|
||||
}
|
||||
|
||||
// GetVNICParams are the paramaters available when communicating with the
|
||||
// ListVNICs API endpoint.
|
||||
type GetVNICParams struct {
|
||||
ID string `url:"vnicId"`
|
||||
}
|
||||
|
||||
// Get returns an individual VNIC.
|
||||
func (s *VNICService) Get(params *GetVNICParams) (VNIC, error) {
|
||||
VNIC := &VNIC{}
|
||||
e := &APIError{}
|
||||
|
||||
_, err := s.client.New().Get(params.ID).Receive(VNIC, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return *VNIC, err
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// VNICAttachmentService enables communicating with the BMCS compute API's VNIC
|
||||
// attachment endpoint.
|
||||
type VNICAttachmentService struct {
|
||||
client *baseClient
|
||||
}
|
||||
|
||||
// NewVNICAttachmentService creates a new VNICAttachmentService for communicating with the
|
||||
// BMCS compute API's instance related endpoints.
|
||||
func NewVNICAttachmentService(s *baseClient) *VNICAttachmentService {
|
||||
return &VNICAttachmentService{
|
||||
client: s.New().Path("vnicAttachments/"),
|
||||
}
|
||||
}
|
||||
|
||||
// VNICAttachment details the attachment of a VNIC to a BMCS instance.
|
||||
type VNICAttachment struct {
|
||||
AvailabilityDomain string `json:"availabilityDomain"`
|
||||
CompartmentID string `json:"compartmentId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
ID string `json:"id"`
|
||||
InstanceID string `json:"instanceId"`
|
||||
LifecycleState string `json:"lifecycleState"`
|
||||
SubnetID string `json:"subnetId"`
|
||||
TimeCreated time.Time `json:"timeCreated"`
|
||||
VNICID string `json:"vnicId"`
|
||||
}
|
||||
|
||||
// ListVnicAttachmentsParams are the paramaters available when communicating
|
||||
// with the ListVnicAttachments API endpoint.
|
||||
type ListVnicAttachmentsParams struct {
|
||||
AvailabilityDomain string `url:"availabilityDomain,omitempty"`
|
||||
CompartmentID string `url:"compartmentId"`
|
||||
InstanceID string `url:"instanceId,omitempty"`
|
||||
VNICID string `url:"vnicId,omitempty"`
|
||||
}
|
||||
|
||||
// List returns an array of VNICAttachments.
|
||||
func (s *VNICAttachmentService) List(params *ListVnicAttachmentsParams) ([]VNICAttachment, error) {
|
||||
vnicAttachments := new([]VNICAttachment)
|
||||
e := new(APIError)
|
||||
|
||||
_, err := s.client.New().Get("").QueryStruct(params).Receive(vnicAttachments, e)
|
||||
err = firstError(err, e)
|
||||
|
||||
return *vnicAttachments, err
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListVNICAttachments(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.image.oc1.phx.a"
|
||||
mux.HandleFunc("/vnicAttachments/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `[{"id":"%s"}]`, id)
|
||||
})
|
||||
|
||||
params := &ListVnicAttachmentsParams{InstanceID: id}
|
||||
|
||||
vnicAttachment, err := client.Compute.VNICAttachments.List(params)
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.VNICAttachments.List() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := []VNICAttachment{{ID: id}}
|
||||
|
||||
if !reflect.DeepEqual(vnicAttachment, want) {
|
||||
t.Errorf("Client.Compute.VNICAttachments.List() returned %+v, want %+v", vnicAttachment, want)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetVNIC(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
id := "ocid1.vnic.oc1.phx.a"
|
||||
path := fmt.Sprintf("/vnics/%s", id)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"id": "%s"}`, id)
|
||||
})
|
||||
|
||||
vnic, err := client.Compute.VNICs.Get(&GetVNICParams{ID: id})
|
||||
if err != nil {
|
||||
t.Errorf("Client.Compute.VNICs.Get() returned error: %v", err)
|
||||
}
|
||||
|
||||
want := &VNIC{ID: id}
|
||||
if reflect.DeepEqual(vnic, want) {
|
||||
t.Errorf("Client.Compute.VNICs.Get() returned %+v, want %+v", vnic, want)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWaitDurationMS = 5000
|
||||
defaultMaxRetries = 0
|
||||
)
|
||||
|
||||
type Waiter struct {
|
||||
WaitDurationMS int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
type WaitableService interface {
|
||||
GetResourceState(id string) (string, error)
|
||||
}
|
||||
|
||||
func stringSliceContains(slice []string, value string) bool {
|
||||
for _, elem := range slice {
|
||||
if elem == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewWaiter creates a waiter with default wait duration and unlimited retry
|
||||
// operations.
|
||||
func NewWaiter() *Waiter {
|
||||
return &Waiter{WaitDurationMS: defaultWaitDurationMS, MaxRetries: defaultMaxRetries}
|
||||
}
|
||||
|
||||
// WaitForResourceToReachState polls a resource that implements WaitableService
|
||||
// repeatedly until it reaches a known state or fails if it reaches an
|
||||
// unexpected state. The duration of the interval and number of polls is
|
||||
// determined by the Waiter configuration.
|
||||
func (w *Waiter) WaitForResourceToReachState(svc WaitableService, id string, waitStates []string, terminalState string) error {
|
||||
for i := 0; w.MaxRetries == 0 || i < w.MaxRetries; i++ {
|
||||
state, err := svc.GetResourceState(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stringSliceContains(waitStates, state) {
|
||||
time.Sleep(time.Duration(w.WaitDurationMS) * time.Millisecond)
|
||||
continue
|
||||
} else if state == terminalState {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unexpected resource state %s, expecting a waiting state %s or terminal state %s ", state, waitStates, terminalState)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Maximum number of retries (%d) exceeded; resource did not reach state %s", w.MaxRetries, terminalState)
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
ValidID = "ID"
|
||||
)
|
||||
|
||||
type testWaitSvc struct {
|
||||
states []string
|
||||
idx int
|
||||
err error
|
||||
}
|
||||
|
||||
func (tw *testWaitSvc) GetResourceState(id string) (string, error) {
|
||||
if id != ValidID {
|
||||
return "", fmt.Errorf("Invalid id %s", id)
|
||||
}
|
||||
if tw.err != nil {
|
||||
return "", tw.err
|
||||
}
|
||||
|
||||
if tw.idx >= len(tw.states) {
|
||||
panic("Invalid test state")
|
||||
}
|
||||
state := tw.states[tw.idx]
|
||||
tw.idx++
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func TestReturnsWhenWaitStateIsReachedImmediately(t *testing.T) {
|
||||
ws := &testWaitSvc{states: []string{"OK"}}
|
||||
w := NewWaiter()
|
||||
err := w.WaitForResourceToReachState(ws, ValidID, []string{}, "OK")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to reach expected state, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReturnsWhenResourceWaitsInValidWaitingState(t *testing.T) {
|
||||
w := &Waiter{WaitDurationMS: 1, MaxRetries: defaultMaxRetries}
|
||||
ws := &testWaitSvc{states: []string{"WAITING", "OK"}}
|
||||
err := w.WaitForResourceToReachState(ws, ValidID, []string{"WAITING"}, "OK")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to reach expected state, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropagatesErrorFromGetter(t *testing.T) {
|
||||
w := NewWaiter()
|
||||
ws := &testWaitSvc{states: []string{}, err: errors.New("ERROR")}
|
||||
err := w.WaitForResourceToReachState(ws, ValidID, []string{"WAITING"}, "OK")
|
||||
if err != ws.err {
|
||||
t.Errorf("Expected error from getter got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportsInvalidTransitionStateAsError(t *testing.T) {
|
||||
w := NewWaiter()
|
||||
tw := &testWaitSvc{states: []string{"UNKNOWN_STATE"}, err: errors.New("ERROR")}
|
||||
err := w.WaitForResourceToReachState(tw, ValidID, []string{"WAITING"}, "OK")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error from getter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsWhenMaxWaitTriesExceeded(t *testing.T) {
|
||||
w := Waiter{WaitDurationMS: 1, MaxRetries: 1}
|
||||
|
||||
ws := &testWaitSvc{states: []string{"WAITING", "OK"}}
|
||||
|
||||
err := w.WaitForResourceToReachState(ws, ValidID, []string{"WAITING"}, "OK")
|
||||
if err == nil {
|
||||
t.Fatal("Expecting error but wait terminated")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
||||
AccessCfg *client.Config
|
||||
|
||||
AccessCfgFile string `mapstructure:"access_cfg_file"`
|
||||
AccessCfgFileAccount string `mapstructure:"access_cfg_file_account"`
|
||||
|
||||
// Access config overrides
|
||||
UserID string `mapstructure:"user_ocid"`
|
||||
TenancyID string `mapstructure:"tenancy_ocid"`
|
||||
Region string `mapstructure:"region"`
|
||||
Fingerprint string `mapstructure:"fingerprint"`
|
||||
KeyFile string `mapstructure:"key_file"`
|
||||
PassPhrase string `mapstructure:"pass_phrase"`
|
||||
|
||||
AvailabilityDomain string `mapstructure:"availability_domain"`
|
||||
CompartmentID string `mapstructure:"compartment_ocid"`
|
||||
|
||||
// Image
|
||||
BaseImageID string `mapstructure:"base_image_ocid"`
|
||||
Shape string `mapstructure:"shape"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
|
||||
// Networking
|
||||
SubnetID string `mapstructure:"subnet_ocid"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, error) {
|
||||
c := &Config{}
|
||||
|
||||
// Decode from template
|
||||
err := config.Decode(c, &config.DecodeOpts{
|
||||
Interpolate: true,
|
||||
InterpolateContext: &c.ctx,
|
||||
}, raws...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to mapstructure Config: %+v", err)
|
||||
}
|
||||
|
||||
// Determine where the SDK config is located
|
||||
var accessCfgFile string
|
||||
if c.AccessCfgFile != "" {
|
||||
accessCfgFile = c.AccessCfgFile
|
||||
} else {
|
||||
accessCfgFile, err = getDefaultBMCSSettingsPath()
|
||||
if err != nil {
|
||||
accessCfgFile = "" // Access cfg might be in template
|
||||
}
|
||||
}
|
||||
|
||||
accessCfg := &client.Config{}
|
||||
|
||||
if accessCfgFile != "" {
|
||||
loadedAccessCfgs, err := client.LoadConfigsFromFile(accessCfgFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid config file %s: %s", accessCfgFile, err)
|
||||
}
|
||||
cfgAccount := "DEFAULT"
|
||||
if c.AccessCfgFileAccount != "" {
|
||||
cfgAccount = c.AccessCfgFileAccount
|
||||
}
|
||||
|
||||
var ok bool
|
||||
accessCfg, ok = loadedAccessCfgs[cfgAccount]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No account section '%s' found in config file %s", cfgAccount, accessCfgFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Override SDK client config with any non-empty template properties
|
||||
|
||||
if c.UserID != "" {
|
||||
accessCfg.User = c.UserID
|
||||
}
|
||||
|
||||
if c.TenancyID != "" {
|
||||
accessCfg.Tenancy = c.TenancyID
|
||||
}
|
||||
|
||||
if c.Region != "" {
|
||||
accessCfg.Region = c.Region
|
||||
} else {
|
||||
accessCfg.Region = "us-phoenix-1"
|
||||
}
|
||||
|
||||
if c.Fingerprint != "" {
|
||||
accessCfg.Fingerprint = c.Fingerprint
|
||||
}
|
||||
|
||||
if c.PassPhrase != "" {
|
||||
accessCfg.PassPhrase = c.PassPhrase
|
||||
}
|
||||
|
||||
if c.KeyFile != "" {
|
||||
accessCfg.KeyFile = c.KeyFile
|
||||
accessCfg.Key, err = client.LoadPrivateKey(accessCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to load private key %s : %s", accessCfg.KeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
|
||||
// Required AccessCfg configuration options
|
||||
|
||||
if accessCfg.User == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'user_ocid' must be specified"))
|
||||
}
|
||||
|
||||
if accessCfg.Tenancy == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'tenancy_ocid' must be specified"))
|
||||
}
|
||||
|
||||
if accessCfg.Region == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'region' must be specified"))
|
||||
}
|
||||
|
||||
if accessCfg.Fingerprint == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'fingerprint' must be specified"))
|
||||
}
|
||||
|
||||
if accessCfg.Key == nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'key_file' must be specified"))
|
||||
}
|
||||
|
||||
c.AccessCfg = accessCfg
|
||||
|
||||
// Required non AccessCfg configuration options
|
||||
|
||||
if c.AvailabilityDomain == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'availability_domain' must be specified"))
|
||||
}
|
||||
|
||||
if c.CompartmentID == "" {
|
||||
c.CompartmentID = accessCfg.Tenancy
|
||||
}
|
||||
|
||||
if c.Shape == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'shape' must be specified"))
|
||||
}
|
||||
|
||||
if c.SubnetID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'subnet_ocid' must be specified"))
|
||||
}
|
||||
|
||||
if c.BaseImageID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'base_image_ocid' must be specified"))
|
||||
}
|
||||
|
||||
if c.ImageName == "" {
|
||||
name, err := interpolate.Render("packer-{{timestamp}}", nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("unable to parse image name: %s", err))
|
||||
} else {
|
||||
c.ImageName = name
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// getDefaultBMCSSettingsPath uses mitchellh/go-homedir to compute the default
|
||||
// config file location.
|
||||
func getDefaultBMCSSettingsPath() (string, error) {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join(home, ".oraclebmc", "config")
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
)
|
||||
|
||||
func testConfig(accessConfFile *os.File) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"availability_domain": "aaaa:PHX-AD-3",
|
||||
"access_cfg_file": accessConfFile.Name(),
|
||||
|
||||
// Image
|
||||
"base_image_ocid": "ocd1...",
|
||||
"shape": "VM.Standard1.1",
|
||||
"image_name": "HelloWorld",
|
||||
|
||||
// Networking
|
||||
"subnet_ocid": "ocd1...",
|
||||
|
||||
// Comm
|
||||
"ssh_username": "opc",
|
||||
}
|
||||
}
|
||||
|
||||
func getField(c *client.Config, field string) string {
|
||||
r := reflect.ValueOf(c)
|
||||
f := reflect.Indirect(r).FieldByName(field)
|
||||
return string(f.String())
|
||||
}
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
// Shared set-up and defered deletion
|
||||
|
||||
cfg, keyFile, err := client.BaseTestConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(keyFile.Name())
|
||||
|
||||
cfgFile, err := client.WriteTestConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(cfgFile.Name())
|
||||
|
||||
// Temporarily set $HOME to temp directory to bypass default
|
||||
// access config loading.
|
||||
|
||||
tmpHome, err := ioutil.TempDir("", "packer_config_test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %+v", err)
|
||||
}
|
||||
defer os.Remove(tmpHome)
|
||||
|
||||
home := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tmpHome)
|
||||
defer os.Setenv("HOME", home)
|
||||
|
||||
// Config tests
|
||||
|
||||
t.Run("BaseConfig", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
_, errs := NewConfig(raw)
|
||||
|
||||
if errs != nil {
|
||||
t.Fatalf("err: %+v", errs)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("NoAccessConfig", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
delete(raw, "access_cfg_file")
|
||||
|
||||
_, errs := NewConfig(raw)
|
||||
|
||||
s := errs.Error()
|
||||
expectedErrors := []string{
|
||||
"'user_ocid'", "'tenancy_ocid'", "'fingerprint'",
|
||||
"'key_file'",
|
||||
}
|
||||
for _, expected := range expectedErrors {
|
||||
if !strings.Contains(s, expected) {
|
||||
t.Errorf("Expected %s to contain '%s'", s, expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AccessConfigTemplateOnly", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
delete(raw, "access_cfg_file")
|
||||
raw["user_ocid"] = "ocid1..."
|
||||
raw["tenancy_ocid"] = "ocid1..."
|
||||
raw["fingerprint"] = "00:00..."
|
||||
raw["key_file"] = keyFile.Name()
|
||||
|
||||
_, errs := NewConfig(raw)
|
||||
|
||||
if errs != nil {
|
||||
t.Fatalf("err: %+v", errs)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("TenancyReadFromAccessCfgFile", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
c, errs := NewConfig(raw)
|
||||
if errs != nil {
|
||||
t.Fatalf("err: %+v", errs)
|
||||
}
|
||||
|
||||
expected := "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
if c.AccessCfg.Tenancy != expected {
|
||||
t.Errorf("Expected tenancy: %s, got %s.", expected, c.AccessCfg.Tenancy)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Test the correct errors are produced when required template keys are
|
||||
// omitted.
|
||||
requiredKeys := []string{"availability_domain", "base_image_ocid", "shape", "subnet_ocid"}
|
||||
for _, k := range requiredKeys {
|
||||
t.Run(k+"_required", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
delete(raw, k)
|
||||
|
||||
_, errs := NewConfig(raw)
|
||||
|
||||
if !strings.Contains(errs.Error(), k) {
|
||||
t.Errorf("Expected '%s' to contain '%s'", errs.Error(), k)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("ImageNameDefaultedIfEmpty", func(t *testing.T) {
|
||||
raw := testConfig(cfgFile)
|
||||
delete(raw, "image_name")
|
||||
|
||||
c, errs := NewConfig(raw)
|
||||
if errs != nil {
|
||||
t.Errorf("Unexpected error(s): %s", errs)
|
||||
}
|
||||
|
||||
if !strings.Contains(c.ImageName, "packer-") {
|
||||
t.Errorf("got default ImageName %q, want image name 'packer-{{timestamp}}'", c.ImageName)
|
||||
}
|
||||
})
|
||||
|
||||
// Test that AccessCfgFile properties are overridden by their
|
||||
// corosponding template keys.
|
||||
accessOverrides := map[string]string{
|
||||
"user_ocid": "User",
|
||||
"tenancy_ocid": "Tenancy",
|
||||
"region": "Region",
|
||||
"fingerprint": "Fingerprint",
|
||||
}
|
||||
for k, v := range accessOverrides {
|
||||
t.Run("AccessCfg."+v+"Overridden", func(t *testing.T) {
|
||||
expected := "override"
|
||||
|
||||
raw := testConfig(cfgFile)
|
||||
raw[k] = expected
|
||||
|
||||
c, errs := NewConfig(raw)
|
||||
if errs != nil {
|
||||
t.Fatalf("err: %+v", errs)
|
||||
}
|
||||
|
||||
accessVal := getField(c.AccessCfg, v)
|
||||
if accessVal != expected {
|
||||
t.Errorf("Expected AccessCfg.%s: %s, got %s", v, expected, accessVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
)
|
||||
|
||||
// Driver interfaces between the builder steps and the BMCS SDK.
|
||||
type Driver interface {
|
||||
CreateInstance(publicKey string) (string, error)
|
||||
CreateImage(id string) (client.Image, error)
|
||||
DeleteImage(id string) error
|
||||
GetInstanceIP(id string) (string, error)
|
||||
TerminateInstance(id string) error
|
||||
WaitForImageCreation(id string) error
|
||||
WaitForInstanceState(id string, waitStates []string, terminalState string) error
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
)
|
||||
|
||||
// driverBMCS implements the Driver interface and communicates with Oracle
|
||||
// BMCS.
|
||||
type driverBMCS struct {
|
||||
client *client.Client
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
// NewDriverBMCS Creates a new driverBMCS with a connected client.
|
||||
func NewDriverBMCS(cfg *Config) (Driver, error) {
|
||||
client, err := client.NewClient(cfg.AccessCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &driverBMCS{client: client, cfg: cfg}, nil
|
||||
}
|
||||
|
||||
// CreateInstance creates a new compute instance.
|
||||
func (d *driverBMCS) CreateInstance(publicKey string) (string, error) {
|
||||
params := &client.LaunchInstanceParams{
|
||||
AvailabilityDomain: d.cfg.AvailabilityDomain,
|
||||
CompartmentID: d.cfg.CompartmentID,
|
||||
ImageID: d.cfg.BaseImageID,
|
||||
Shape: d.cfg.Shape,
|
||||
SubnetID: d.cfg.SubnetID,
|
||||
Metadata: map[string]string{
|
||||
"ssh_authorized_keys": publicKey,
|
||||
},
|
||||
}
|
||||
instance, err := d.client.Compute.Instances.Launch(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return instance.ID, nil
|
||||
}
|
||||
|
||||
// CreateImage creates a new custom image.
|
||||
func (d *driverBMCS) CreateImage(id string) (client.Image, error) {
|
||||
params := &client.CreateImageParams{
|
||||
CompartmentID: d.cfg.CompartmentID,
|
||||
InstanceID: id,
|
||||
DisplayName: d.cfg.ImageName,
|
||||
}
|
||||
image, err := d.client.Compute.Images.Create(params)
|
||||
if err != nil {
|
||||
return client.Image{}, err
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// DeleteImage deletes a custom image.
|
||||
func (d *driverBMCS) DeleteImage(id string) error {
|
||||
return d.client.Compute.Images.Delete(&client.DeleteImageParams{ID: id})
|
||||
}
|
||||
|
||||
// GetInstanceIP returns the public IP corresponding to the given instance id.
|
||||
func (d *driverBMCS) GetInstanceIP(id string) (string, error) {
|
||||
// get nvic and cross ref to find pub ip address
|
||||
vnics, err := d.client.Compute.VNICAttachments.List(
|
||||
&client.ListVnicAttachmentsParams{
|
||||
InstanceID: id,
|
||||
CompartmentID: d.cfg.CompartmentID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(vnics) < 1 {
|
||||
return "", errors.New("instance has zero VNICs")
|
||||
}
|
||||
|
||||
vnic, err := d.client.Compute.VNICs.Get(&client.GetVNICParams{ID: vnics[0].VNICID})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error getting VNIC details: %s", err)
|
||||
}
|
||||
|
||||
return vnic.PublicIP, nil
|
||||
}
|
||||
|
||||
// TerminateInstance terminates a compute instance.
|
||||
func (d *driverBMCS) TerminateInstance(id string) error {
|
||||
params := &client.TerminateInstanceParams{ID: id}
|
||||
return d.client.Compute.Instances.Terminate(params)
|
||||
}
|
||||
|
||||
// WaitForImageCreation waits for a provisioning custom image to reach the
|
||||
// "AVAILABLE" state.
|
||||
func (d *driverBMCS) WaitForImageCreation(id string) error {
|
||||
return client.NewWaiter().WaitForResourceToReachState(
|
||||
d.client.Compute.Images,
|
||||
id,
|
||||
[]string{"PROVISIONING"},
|
||||
"AVAILABLE",
|
||||
)
|
||||
}
|
||||
|
||||
// WaitForInstanceState waits for an instance to reach the a given terminal
|
||||
// state.
|
||||
func (d *driverBMCS) WaitForInstanceState(id string, waitStates []string, terminalState string) error {
|
||||
return client.NewWaiter().WaitForResourceToReachState(
|
||||
d.client.Compute.Instances,
|
||||
id,
|
||||
waitStates,
|
||||
terminalState,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
)
|
||||
|
||||
// driverMock implements the Driver interface and communicates with Oracle
|
||||
// BMCS.
|
||||
type driverMock struct {
|
||||
CreateInstanceID string
|
||||
CreateInstanceErr error
|
||||
|
||||
CreateImageID string
|
||||
CreateImageErr error
|
||||
|
||||
DeleteImageID string
|
||||
DeleteImageErr error
|
||||
|
||||
GetInstanceIPErr error
|
||||
|
||||
TerminateInstanceID string
|
||||
TerminateInstanceErr error
|
||||
|
||||
WaitForImageCreationErr error
|
||||
|
||||
WaitForInstanceStateErr error
|
||||
}
|
||||
|
||||
// CreateInstance creates a new compute instance.
|
||||
func (d *driverMock) CreateInstance(publicKey string) (string, error) {
|
||||
if d.CreateInstanceErr != nil {
|
||||
return "", d.CreateInstanceErr
|
||||
}
|
||||
|
||||
d.CreateInstanceID = "ocid1..."
|
||||
|
||||
return d.CreateInstanceID, nil
|
||||
}
|
||||
|
||||
// CreateImage creates a new custom image.
|
||||
func (d *driverMock) CreateImage(id string) (client.Image, error) {
|
||||
if d.CreateImageErr != nil {
|
||||
return client.Image{}, d.CreateImageErr
|
||||
}
|
||||
d.CreateImageID = id
|
||||
return client.Image{ID: id}, nil
|
||||
}
|
||||
|
||||
// DeleteImage mocks deleting a custom image.
|
||||
func (d *driverMock) DeleteImage(id string) error {
|
||||
if d.DeleteImageErr != nil {
|
||||
return d.DeleteImageErr
|
||||
}
|
||||
|
||||
d.DeleteImageID = id
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInstanceIP returns the public IP corresponding to the given instance id.
|
||||
func (d *driverMock) GetInstanceIP(id string) (string, error) {
|
||||
if d.GetInstanceIPErr != nil {
|
||||
return "", d.GetInstanceIPErr
|
||||
}
|
||||
return "ip", nil
|
||||
}
|
||||
|
||||
// TerminateInstance terminates a compute instance.
|
||||
func (d *driverMock) TerminateInstance(id string) error {
|
||||
if d.TerminateInstanceErr != nil {
|
||||
return d.TerminateInstanceErr
|
||||
}
|
||||
|
||||
d.TerminateInstanceID = id
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitForImageCreation waits for a provisioning custom image to reach the
|
||||
// "AVAILABLE" state.
|
||||
func (d *driverMock) WaitForImageCreation(id string) error {
|
||||
return d.WaitForImageCreationErr
|
||||
}
|
||||
|
||||
// WaitForInstanceState waits for an instance to reach the a given terminal
|
||||
// state.
|
||||
func (d *driverMock) WaitForInstanceState(id string, waitStates []string, terminalState string) error {
|
||||
return d.WaitForInstanceStateErr
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
packerssh "github.com/hashicorp/packer/communicator/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func commHost(state multistep.StateBag) (string, error) {
|
||||
ipAddress := state.Get("instance_ip").(string)
|
||||
return ipAddress, nil
|
||||
}
|
||||
|
||||
// SSHConfig returns a function that can be used for the SSH communicator
|
||||
// config for connecting to the instance created over SSH using the private key
|
||||
// or password.
|
||||
func SSHConfig(username, password string) func(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
return func(state multistep.StateBag) (*ssh.ClientConfig, error) {
|
||||
privateKey, hasKey := state.GetOk("privateKey")
|
||||
if hasKey {
|
||||
|
||||
signer, err := ssh.ParsePrivateKey([]byte(privateKey.(string)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
}
|
||||
return &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
return &ssh.ClientConfig{
|
||||
User: username,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(password),
|
||||
ssh.KeyboardInteractive(packerssh.PasswordKeyboardInteractive(password)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
type stepCreateInstance struct{}
|
||||
|
||||
func (s *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
var (
|
||||
driver = state.Get("driver").(Driver)
|
||||
ui = state.Get("ui").(packer.Ui)
|
||||
publicKey = state.Get("publicKey").(string)
|
||||
)
|
||||
|
||||
ui.Say("Creating instance...")
|
||||
|
||||
instanceID, err := driver.CreateInstance(publicKey)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Problem creating instance: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("instance_id", instanceID)
|
||||
|
||||
ui.Say(fmt.Sprintf("Created instance (%s).", instanceID))
|
||||
|
||||
ui.Say("Waiting for instance to enter 'RUNNING' state...")
|
||||
|
||||
if err = driver.WaitForInstanceState(instanceID, []string{"STARTING", "PROVISIONING"}, "RUNNING"); err != nil {
|
||||
err = fmt.Errorf("Error waiting for instance to start: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Instance 'RUNNING'.")
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
idRaw, ok := state.GetOk("instance_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id := idRaw.(string)
|
||||
|
||||
ui.Say(fmt.Sprintf("Terminating instance (%s)...", id))
|
||||
|
||||
if err := driver.TerminateInstance(id); err != nil {
|
||||
err = fmt.Errorf("Error terminating instance. Please terminate manually: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return
|
||||
}
|
||||
|
||||
err := driver.WaitForInstanceState(id, []string{"TERMINATING"}, "TERMINATED")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error terminating instance. Please terminate manually: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return
|
||||
}
|
||||
|
||||
ui.Say("Terminated instance.")
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
func TestStepCreateInstance(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("publicKey", "key")
|
||||
|
||||
step := new(stepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
instanceIDRaw, ok := state.GetOk("instance_id")
|
||||
if !ok {
|
||||
t.Fatalf("should have machine")
|
||||
}
|
||||
|
||||
step.Cleanup(state)
|
||||
|
||||
if driver.TerminateInstanceID != instanceIDRaw.(string) {
|
||||
t.Fatalf(
|
||||
"should've deleted instance (%s != %s)",
|
||||
driver.TerminateInstanceID, instanceIDRaw.(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_CreateInstanceErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("publicKey", "key")
|
||||
|
||||
step := new(stepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
driver.CreateInstanceErr = errors.New("error")
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("instance_id"); ok {
|
||||
t.Fatalf("should NOT have instance_id")
|
||||
}
|
||||
|
||||
step.Cleanup(state)
|
||||
|
||||
if driver.TerminateInstanceID != "" {
|
||||
t.Fatalf("Should not have tried to terminate an instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_WaitForInstanceStateErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("publicKey", "key")
|
||||
|
||||
step := new(stepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
driver.WaitForInstanceStateErr = errors.New("error")
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_TerminateInstanceErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("publicKey", "key")
|
||||
|
||||
step := new(stepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
_, ok := state.GetOk("instance_id")
|
||||
if !ok {
|
||||
t.Fatalf("should have machine")
|
||||
}
|
||||
|
||||
driver.TerminateInstanceErr = errors.New("error")
|
||||
step.Cleanup(state)
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstanceCleanup_WaitForInstanceStateErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("publicKey", "key")
|
||||
|
||||
step := new(stepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
driver.WaitForInstanceStateErr = errors.New("error")
|
||||
step.Cleanup(state)
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
type stepImage struct{}
|
||||
|
||||
func (s *stepImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||
var (
|
||||
driver = state.Get("driver").(Driver)
|
||||
ui = state.Get("ui").(packer.Ui)
|
||||
instanceID = state.Get("instance_id").(string)
|
||||
)
|
||||
|
||||
ui.Say("Creating image from instance...")
|
||||
|
||||
image, err := driver.CreateImage(instanceID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error creating image from instance: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
err = driver.WaitForImageCreation(image.ID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error waiting for image creation to finish: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// TODO(apryde): This is stale as .LifecycleState has changed to
|
||||
// AVAILABLE at this point. Does it matter?
|
||||
state.Put("image", image)
|
||||
|
||||
ui.Say("Image created.")
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepImage) Cleanup(state multistep.StateBag) {
|
||||
// Nothing to do
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
func TestStepImage(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("instance_id", "ocid1...")
|
||||
|
||||
step := new(stepImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("image"); !ok {
|
||||
t.Fatalf("should have image")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepImage_CreateImageErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("instance_id", "ocid1...")
|
||||
|
||||
step := new(stepImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
driver.CreateImageErr = errors.New("error")
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("image"); ok {
|
||||
t.Fatalf("should NOT have image")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepImage_WaitForImageCreationErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("instance_id", "ocid1...")
|
||||
|
||||
step := new(stepImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
driver.WaitForImageCreationErr = errors.New("error")
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("image"); ok {
|
||||
t.Fatalf("should not have image")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
type stepInstanceInfo struct{}
|
||||
|
||||
func (s *stepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||
var (
|
||||
driver = state.Get("driver").(Driver)
|
||||
ui = state.Get("ui").(packer.Ui)
|
||||
id = state.Get("instance_id").(string)
|
||||
)
|
||||
|
||||
ip, err := driver.GetInstanceIP(id)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error getting instance's public IP: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("instance_ip", ip)
|
||||
|
||||
ui.Say(fmt.Sprintf("Instance has public IP: %s.", ip))
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepInstanceInfo) Cleanup(state multistep.StateBag) {
|
||||
// no cleanup
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
)
|
||||
|
||||
func TestInstanceInfo(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("instance_id", "ocid1...")
|
||||
|
||||
step := new(stepInstanceInfo)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
instanceIPRaw, ok := state.GetOk("instance_ip")
|
||||
if !ok {
|
||||
t.Fatalf("should have instance_ip")
|
||||
}
|
||||
|
||||
if instanceIPRaw.(string) != "ip" {
|
||||
t.Fatalf("should've got ip ('%s' != 'ip')", instanceIPRaw.(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceInfo_GetInstanceIPErr(t *testing.T) {
|
||||
state := testState()
|
||||
state.Put("instance_id", "ocid1...")
|
||||
|
||||
step := new(stepInstanceInfo)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
driver := state.Get("driver").(*driverMock)
|
||||
driver.GetInstanceIPErr = errors.New("error")
|
||||
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatalf("should have error")
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("instance_ip"); ok {
|
||||
t.Fatalf("should NOT have instance_ip")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/mitchellh/multistep"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type stepKeyPair struct {
|
||||
Debug bool
|
||||
DebugKeyPath string
|
||||
PrivateKeyFile string
|
||||
}
|
||||
|
||||
func (s *stepKeyPair) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
if s.PrivateKeyFile != "" {
|
||||
privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error loading configured private key file: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
key, err := ssh.ParsePrivateKey(privateKeyBytes)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error parsing 'ssh_private_key_file': %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("publicKey", string(ssh.MarshalAuthorizedKey(key.PublicKey())))
|
||||
state.Put("privateKey", string(privateKeyBytes))
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Creating temporary ssh key for instance...")
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error creating temporary SSH key: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// ASN.1 DER encoded form
|
||||
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||
privBlk := pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: privDer}
|
||||
|
||||
// Set the private key in the statebag for later
|
||||
state.Put("privateKey", string(pem.EncodeToMemory(&privBlk)))
|
||||
|
||||
// Marshal the public key into SSH compatible format
|
||||
pub, err := ssh.NewPublicKey(&priv.PublicKey)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error marshaling temporary SSH public key: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub))
|
||||
state.Put("publicKey", pubSSHFormat)
|
||||
|
||||
// If we're in debug mode, output the private key to the working
|
||||
// directory.
|
||||
if s.Debug {
|
||||
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||
f, err := os.Create(s.DebugKeyPath)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error saving debug key: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write the key out
|
||||
if _, err := f.Write(pem.EncodeToMemory(&privBlk)); err != nil {
|
||||
err = fmt.Errorf("Error saving debug key: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Chmod it so that it is SSH ready
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := f.Chmod(0600); err != nil {
|
||||
err = fmt.Errorf("Error setting permissions of debug key: %s", err)
|
||||
ui.Error(err.Error())
|
||||
state.Put("error", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepKeyPair) Cleanup(state multistep.StateBag) {
|
||||
// Nothing to do
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package bmcs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/mitchellh/multistep"
|
||||
|
||||
client "github.com/hashicorp/packer/builder/oracle/bmcs/client"
|
||||
)
|
||||
|
||||
// TODO(apryde): It would be good not to have to write a key file to disk to
|
||||
// load the config.
|
||||
func baseTestConfig() *Config {
|
||||
_, keyFile, err := client.BaseTestConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg, err := NewConfig(map[string]interface{}{
|
||||
"availability_domain": "aaaa:PHX-AD-3",
|
||||
|
||||
// Image
|
||||
"base_image_ocid": "ocd1...",
|
||||
"shape": "VM.Standard1.1",
|
||||
"image_name": "HelloWorld",
|
||||
|
||||
// Networking
|
||||
"subnet_ocid": "ocd1...",
|
||||
|
||||
// AccessConfig
|
||||
"user_ocid": "ocid1...",
|
||||
"tenancy_ocid": "ocid1...",
|
||||
"fingerprint": "00:00...",
|
||||
"key_file": keyFile.Name(),
|
||||
|
||||
// Comm
|
||||
"ssh_username": "opc",
|
||||
})
|
||||
|
||||
// Once we have a config object they key file isn't re-read so we can
|
||||
// remove it now.
|
||||
os.Remove(keyFile.Name())
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func testState() multistep.StateBag {
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", baseTestConfig())
|
||||
state.Put("driver", &driverMock{})
|
||||
state.Put("hook", &packer.MockHook{})
|
||||
state.Put("ui", &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
})
|
||||
return state
|
||||
}
|
|
@ -31,6 +31,7 @@ import (
|
|||
nullbuilder "github.com/hashicorp/packer/builder/null"
|
||||
oneandonebuilder "github.com/hashicorp/packer/builder/oneandone"
|
||||
openstackbuilder "github.com/hashicorp/packer/builder/openstack"
|
||||
oraclebmcsbuilder "github.com/hashicorp/packer/builder/oracle/bmcs"
|
||||
parallelsisobuilder "github.com/hashicorp/packer/builder/parallels/iso"
|
||||
parallelspvmbuilder "github.com/hashicorp/packer/builder/parallels/pvm"
|
||||
profitbricksbuilder "github.com/hashicorp/packer/builder/profitbricks"
|
||||
|
@ -96,6 +97,7 @@ var Builders = map[string]packer.Builder{
|
|||
"null": new(nullbuilder.Builder),
|
||||
"oneandone": new(oneandonebuilder.Builder),
|
||||
"openstack": new(openstackbuilder.Builder),
|
||||
"oracle-bmcs": new(oraclebmcsbuilder.Builder),
|
||||
"parallels-iso": new(parallelsisobuilder.Builder),
|
||||
"parallels-pvm": new(parallelspvmbuilder.Builder),
|
||||
"profitbricks": new(profitbricksbuilder.Builder),
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
with Oracle Bare Metal Cloud Services (BMCS). The builder takes an
|
||||
Oracle-provided base image, runs any provisioning necessary on the base image
|
||||
after launching it, and finally snapshots it creating a reusable custom
|
||||
image.
|
||||
---
|
||||
|
||||
# Oracle Bare Metal Cloud Services (BMCS) Builder
|
||||
|
||||
Type: `oracle-bmcs`
|
||||
|
||||
The `oracle-bmcs` Packer builder is able to create new custom images for use
|
||||
with [Oracle Bare Metal Cloud Services](https://cloud.oracle.com/en_US/bare-metal-compute)
|
||||
(BMCS). The builder takes an Oracle-provided base image, runs any provisioning
|
||||
necessary on the base image after launching it, and finally snapshots it
|
||||
creating a reusable custom image.
|
||||
|
||||
It is recommended that you familiarise yourself with the
|
||||
[Key Concepts and Terminology](https://docs.us-phoenix-1.oraclecloud.com/Content/GSG/Concepts/concepts.htm)
|
||||
prior to using this builder if you have not done so already.
|
||||
|
||||
The builder _does not_ manage images. Once it creates an image, it is up to you
|
||||
to use it or delete it.
|
||||
|
||||
## Authorization
|
||||
|
||||
The Oracle BMCS API requires that requests be signed with the RSA public key
|
||||
associated with your [IAM](https://docs.us-phoenix-1.oraclecloud.com/Content/Identity/Concepts/overview.htm)
|
||||
user account. For a comprehensive example of how to configure the required
|
||||
authentication see the documentation on
|
||||
[Required Keys and OCIDs](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/apisigningkey.htm)
|
||||
([Oracle Cloud IDs](https://docs.us-phoenix-1.oraclecloud.com/Content/General/Concepts/identifiers.htm)).
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
There are many configuration options available for the `oracle-bmcs` builder.
|
||||
In addition to the options listed here, a
|
||||
[communicator](/docs/templates/communicator.html) can be configured for this
|
||||
builder.
|
||||
|
||||
### Required
|
||||
|
||||
- `availability_domain` (string) - The name of the
|
||||
[Availability Domain](https://docs.us-phoenix-1.oraclecloud.com/Content/General/Concepts/regions.htm)
|
||||
within which a new instance is launched and provisioned.
|
||||
The names of the Availability Domains have a prefix that is specific to
|
||||
your [tenancy](https://docs.us-phoenix-1.oraclecloud.com/Content/GSG/Concepts/concepts.htm#two).
|
||||
|
||||
To get a list of the Availability Domains, use the
|
||||
[ListAvailabilityDomains](https://docs.us-phoenix-1.oraclecloud.com/api/#/en/identity/latest/AvailabilityDomain/ListAvailabilityDomains)
|
||||
operation, which is available in the IAM Service API.
|
||||
|
||||
- `base_image_ocid` (string) - The OCID of the
|
||||
[Oracle-provided base image](https://docs.us-phoenix-1.oraclecloud.com/Content/Compute/References/images.htm)
|
||||
to use. This is the unique identifier of the image that will be used to
|
||||
launch a new instance and provision it.
|
||||
|
||||
To get a list of the accepted image OCIDs, use the
|
||||
[ListImages](https://docs.us-phoenix-1.oraclecloud.com/api/#/en/iaas/latest/Image/ListImages)
|
||||
operation available in the Core Services API.
|
||||
|
||||
- `compartment_ocid` (string) - The OCID of the
|
||||
[compartment](https://docs.us-phoenix-1.oraclecloud.com/Content/GSG/Tasks/choosingcompartments.htm)
|
||||
|
||||
- `fingerprint` (string) - Fingerprint for the BMCS API signing key.
|
||||
Overrides value provided by the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
if present.
|
||||
|
||||
- `shape` (string) - The template that determines the number of
|
||||
CPUs, amount of memory, and other resources allocated to a newly created
|
||||
instance.
|
||||
|
||||
To get a list of the available shapes, use the
|
||||
[ListShapes](https://docs.us-phoenix-1.oraclecloud.com/api/#/en/iaas/20160918/Shape/ListShapes)
|
||||
operation available in the Core Services API.
|
||||
|
||||
- `subnet_ocid` (string) - The name of the subnet within which a new instance
|
||||
is launched and provisioned.
|
||||
|
||||
To get a list of your subnets, use the
|
||||
[ListSubnets](https://docs.us-phoenix-1.oraclecloud.com/api/#/en/iaas/latest/Subnet/ListSubnets)
|
||||
operation available in the Core Services API.
|
||||
|
||||
Note: the subnet must be configured to allow access via your chosen
|
||||
[communicator](/docs/templates/communicator.html) (communicator defaults to
|
||||
[SSH tcp/22](/docs/templates/communicator.html#ssh_port)).
|
||||
|
||||
|
||||
### Optional
|
||||
|
||||
- `access_cfg_file` (string) - The path to the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm).
|
||||
Defaults to `$HOME/.oraclebmc/config`.
|
||||
|
||||
- `access_cfg_file_account` (string) - The specific account in the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
to use. Defaults to `DEFAULT`.
|
||||
|
||||
- `image_name` (string) - The name to assign to the resulting custom image.
|
||||
|
||||
- `key_file` (string) - Full path and filename of the BMCS API signing key.
|
||||
Overrides value provided by the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
if present.
|
||||
|
||||
- `pass_phrase` (string) - Pass phrase used to decrypt the BMCS API signing
|
||||
key. Overrides value provided by the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
if present.
|
||||
|
||||
- `region` (string) - An Oracle Bare Metal Cloud Services region. Overrides
|
||||
value provided by the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
if present.
|
||||
|
||||
- `tenancy_ocid` (string) - The OCID of your tenancy. Overrides value provided
|
||||
by the
|
||||
[BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
if present.
|
||||
|
||||
- `user_ocid` (string) - The OCID of the user calling the BMCS API. Overrides
|
||||
value provided by the [BMCS config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm)
|
||||
if present.
|
||||
|
||||
|
||||
## Basic Example
|
||||
|
||||
Here is a basic example. Note that account specific configuration has been
|
||||
substituted with the letter `a` and OCIDS have been shortened for brevity.
|
||||
|
||||
``` {.javascript}
|
||||
{
|
||||
"type": "oracle-bmcs",
|
||||
"compartment_ocid": "ocid1.compartment.oc1..aaa",
|
||||
"availability_domain": "aaaa:PHX-AD-1",
|
||||
"subnet_ocid": "ocid1.subnet.oc1..aaa",
|
||||
"base_image_ocid": "ocid1.image.oc1.phx.aaaaaaaa5yu6pw3riqtuhxzov7fdngi4tsteganmao54nq3pyxu3hxcuzmoa",
|
||||
"ssh_username": "opc",
|
||||
"shape": "VM.Standard1.1",
|
||||
"image_name": "ExampleImage"
|
||||
}
|
||||
```
|
|
@ -122,6 +122,9 @@
|
|||
<li<%= sidebar_current("docs-builders-openstack") %>>
|
||||
<a href="/docs/builders/openstack.html">OpenStack</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-builders-oracle-bmcs") %>>
|
||||
<a href="/docs/builders/oracle-bmcs.html">Oracle BMCS</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-builders-parallels") %>>
|
||||
<a href="/docs/builders/parallels.html">Parallels</a>
|
||||
<ul class="nav">
|
||||
|
|
Loading…
Reference in New Issue