Merge pull request #4554 from prydie/f-oracle-bmcs

Oracle Bare Metal Cloud Services (BMCS) builder
This commit is contained in:
Matthew Hooker 2017-09-11 09:42:14 -07:00 committed by GitHub
commit 624b1e5110
42 changed files with 3470 additions and 0 deletions

View File

@ -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.

View File

@ -0,0 +1 @@
Copyright (c) 2017 Oracle America, Inc.

View File

@ -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)
}

View File

@ -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")
}
}

View File

@ -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()
}
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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))
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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.")
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),

View File

@ -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"
}
```

View File

@ -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">