335 lines
11 KiB
Go
335 lines
11 KiB
Go
//go:generate struct-markdown
|
|
|
|
package openstack
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/gophercloud/gophercloud"
|
|
"github.com/gophercloud/gophercloud/openstack"
|
|
"github.com/gophercloud/utils/openstack/clientconfig"
|
|
"github.com/hashicorp/go-cleanhttp"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
|
)
|
|
|
|
// AccessConfig is for common configuration related to openstack access
|
|
type AccessConfig struct {
|
|
// The username or id used to connect to the OpenStack service. If not
|
|
// specified, Packer will use the environment variable OS_USERNAME or
|
|
// OS_USERID, if set. This is not required if using access token or
|
|
// application credential instead of password, or if using cloud.yaml.
|
|
Username string `mapstructure:"username" required:"true"`
|
|
// Sets username
|
|
UserID string `mapstructure:"user_id"`
|
|
// The password used to connect to the OpenStack service. If not specified,
|
|
// Packer will use the environment variables OS_PASSWORD, if set. This is
|
|
// not required if using access token or application credential instead of
|
|
// password, or if using cloud.yaml.
|
|
Password string `mapstructure:"password" required:"true"`
|
|
// The URL to the OpenStack Identity service. If not specified, Packer will
|
|
// use the environment variables OS_AUTH_URL, if set. This is not required
|
|
// if using cloud.yaml.
|
|
IdentityEndpoint string `mapstructure:"identity_endpoint" required:"true"`
|
|
// The tenant ID or name to boot the instance into. Some OpenStack
|
|
// installations require this. If not specified, Packer will use the
|
|
// environment variable OS_TENANT_NAME or OS_TENANT_ID, if set. Tenant is
|
|
// also called Project in later versions of OpenStack.
|
|
TenantID string `mapstructure:"tenant_id" required:"false"`
|
|
TenantName string `mapstructure:"tenant_name"`
|
|
DomainID string `mapstructure:"domain_id"`
|
|
// The Domain name or ID you are authenticating with. OpenStack
|
|
// installations require this if identity v3 is used. Packer will use the
|
|
// environment variable OS_DOMAIN_NAME or OS_DOMAIN_ID, if set.
|
|
DomainName string `mapstructure:"domain_name" required:"false"`
|
|
// Whether or not the connection to OpenStack can be done over an insecure
|
|
// connection. By default this is false.
|
|
Insecure bool `mapstructure:"insecure" required:"false"`
|
|
// The name of the region, such as "DFW", in which to launch the server to
|
|
// create the image. If not specified, Packer will use the environment
|
|
// variable OS_REGION_NAME, if set.
|
|
Region string `mapstructure:"region" required:"false"`
|
|
// The endpoint type to use. Can be any of "internal", "internalURL",
|
|
// "admin", "adminURL", "public", and "publicURL". By default this is
|
|
// "public".
|
|
EndpointType string `mapstructure:"endpoint_type" required:"false"`
|
|
// Custom CA certificate file path. If omitted the OS_CACERT environment
|
|
// variable can be used.
|
|
CACertFile string `mapstructure:"cacert" required:"false"`
|
|
// Client certificate file path for SSL client authentication. If omitted
|
|
// the OS_CERT environment variable can be used.
|
|
ClientCertFile string `mapstructure:"cert" required:"false"`
|
|
// Client private key file path for SSL client authentication. If omitted
|
|
// the OS_KEY environment variable can be used.
|
|
ClientKeyFile string `mapstructure:"key" required:"false"`
|
|
// the token (id) to use with token based authorization. Packer will use
|
|
// the environment variable OS_TOKEN, if set.
|
|
Token string `mapstructure:"token" required:"false"`
|
|
// The application credential name to use with application credential based
|
|
// authorization. Packer will use the environment variable
|
|
// OS_APPLICATION_CREDENTIAL_NAME, if set.
|
|
ApplicationCredentialName string `mapstructure:"application_credential_name" required:"false"`
|
|
// The application credential id to use with application credential based
|
|
// authorization. Packer will use the environment variable
|
|
// OS_APPLICATION_CREDENTIAL_ID, if set.
|
|
ApplicationCredentialID string `mapstructure:"application_credential_id" required:"false"`
|
|
// The application credential secret to use with application credential
|
|
// based authorization. Packer will use the environment variable
|
|
// OS_APPLICATION_CREDENTIAL_SECRET, if set.
|
|
ApplicationCredentialSecret string `mapstructure:"application_credential_secret" required:"false"`
|
|
// An entry in a `clouds.yaml` file. See the OpenStack os-client-config
|
|
// [documentation](https://docs.openstack.org/os-client-config/latest/user/configuration.html)
|
|
// for more information about `clouds.yaml` files. If omitted, the
|
|
// `OS_CLOUD` environment variable is used.
|
|
Cloud string `mapstructure:"cloud" required:"false"`
|
|
|
|
osClient *gophercloud.ProviderClient
|
|
}
|
|
|
|
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
|
|
if c.EndpointType != "internal" && c.EndpointType != "internalURL" &&
|
|
c.EndpointType != "admin" && c.EndpointType != "adminURL" &&
|
|
c.EndpointType != "public" && c.EndpointType != "publicURL" &&
|
|
c.EndpointType != "" {
|
|
return []error{fmt.Errorf("Invalid endpoint type provided")}
|
|
}
|
|
|
|
// Legacy RackSpace stuff. We're keeping this around to keep things BC.
|
|
if c.Password == "" {
|
|
c.Password = os.Getenv("SDK_PASSWORD")
|
|
}
|
|
if c.Region == "" {
|
|
c.Region = os.Getenv("SDK_REGION")
|
|
}
|
|
if c.TenantName == "" {
|
|
c.TenantName = os.Getenv("SDK_PROJECT")
|
|
}
|
|
if c.Username == "" {
|
|
c.Username = os.Getenv("SDK_USERNAME")
|
|
}
|
|
// End RackSpace
|
|
|
|
if c.Cloud == "" {
|
|
c.Cloud = os.Getenv("OS_CLOUD")
|
|
}
|
|
if c.Region == "" {
|
|
c.Region = os.Getenv("OS_REGION_NAME")
|
|
}
|
|
|
|
if c.CACertFile == "" {
|
|
c.CACertFile = os.Getenv("OS_CACERT")
|
|
}
|
|
if c.ClientCertFile == "" {
|
|
c.ClientCertFile = os.Getenv("OS_CERT")
|
|
}
|
|
if c.ClientKeyFile == "" {
|
|
c.ClientKeyFile = os.Getenv("OS_KEY")
|
|
}
|
|
|
|
clientOpts := new(clientconfig.ClientOpts)
|
|
|
|
// If a cloud entry was given, base AuthOptions on a clouds.yaml file.
|
|
if c.Cloud != "" {
|
|
clientOpts.Cloud = c.Cloud
|
|
|
|
cloud, err := clientconfig.GetCloudFromYAML(clientOpts)
|
|
if err != nil {
|
|
return []error{err}
|
|
}
|
|
|
|
if c.Region == "" && cloud.RegionName != "" {
|
|
c.Region = cloud.RegionName
|
|
}
|
|
} else {
|
|
authInfo := &clientconfig.AuthInfo{
|
|
AuthURL: c.IdentityEndpoint,
|
|
DomainID: c.DomainID,
|
|
DomainName: c.DomainName,
|
|
Password: c.Password,
|
|
ProjectID: c.TenantID,
|
|
ProjectName: c.TenantName,
|
|
Token: c.Token,
|
|
Username: c.Username,
|
|
UserID: c.UserID,
|
|
}
|
|
clientOpts.AuthInfo = authInfo
|
|
}
|
|
|
|
ao, err := clientconfig.AuthOptions(clientOpts)
|
|
if err != nil {
|
|
return []error{err}
|
|
}
|
|
|
|
// Make sure we reauth as needed
|
|
ao.AllowReauth = true
|
|
|
|
// Override values if we have them in our config
|
|
overrides := []struct {
|
|
From, To *string
|
|
}{
|
|
{&c.Username, &ao.Username},
|
|
{&c.UserID, &ao.UserID},
|
|
{&c.Password, &ao.Password},
|
|
{&c.IdentityEndpoint, &ao.IdentityEndpoint},
|
|
{&c.TenantID, &ao.TenantID},
|
|
{&c.TenantName, &ao.TenantName},
|
|
{&c.DomainID, &ao.DomainID},
|
|
{&c.DomainName, &ao.DomainName},
|
|
{&c.Token, &ao.TokenID},
|
|
{&c.ApplicationCredentialName, &ao.ApplicationCredentialName},
|
|
{&c.ApplicationCredentialID, &ao.ApplicationCredentialID},
|
|
{&c.ApplicationCredentialSecret, &ao.ApplicationCredentialSecret},
|
|
}
|
|
for _, s := range overrides {
|
|
if *s.From != "" {
|
|
*s.To = *s.From
|
|
}
|
|
}
|
|
|
|
// Build the client itself
|
|
client, err := openstack.NewClient(ao.IdentityEndpoint)
|
|
if err != nil {
|
|
return []error{err}
|
|
}
|
|
|
|
tls_config := &tls.Config{}
|
|
|
|
if c.CACertFile != "" {
|
|
caCert, err := ioutil.ReadFile(c.CACertFile)
|
|
if err != nil {
|
|
return []error{err}
|
|
}
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
tls_config.RootCAs = caCertPool
|
|
}
|
|
|
|
// If we have insecure set, then create a custom HTTP client that ignores
|
|
// SSL errors.
|
|
if c.Insecure {
|
|
tls_config.InsecureSkipVerify = true
|
|
}
|
|
|
|
if c.ClientCertFile != "" && c.ClientKeyFile != "" {
|
|
cert, err := tls.LoadX509KeyPair(c.ClientCertFile, c.ClientKeyFile)
|
|
if err != nil {
|
|
return []error{err}
|
|
}
|
|
|
|
tls_config.Certificates = []tls.Certificate{cert}
|
|
}
|
|
|
|
transport := cleanhttp.DefaultTransport()
|
|
transport.TLSClientConfig = tls_config
|
|
client.HTTPClient.Transport = transport
|
|
|
|
// Auth
|
|
err = openstack.Authenticate(client, *ao)
|
|
if err != nil {
|
|
return []error{err}
|
|
}
|
|
|
|
c.osClient = client
|
|
return nil
|
|
}
|
|
|
|
func (c *AccessConfig) enableDebug(ui packersdk.Ui) {
|
|
c.osClient.HTTPClient = http.Client{
|
|
Transport: &DebugRoundTripper{
|
|
ui: ui,
|
|
rt: c.osClient.HTTPClient.Transport,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
|
|
return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
|
|
Region: c.Region,
|
|
Availability: c.getEndpointType(),
|
|
})
|
|
}
|
|
|
|
func (c *AccessConfig) imageV2Client() (*gophercloud.ServiceClient, error) {
|
|
return openstack.NewImageServiceV2(c.osClient, gophercloud.EndpointOpts{
|
|
Region: c.Region,
|
|
Availability: c.getEndpointType(),
|
|
})
|
|
}
|
|
|
|
func (c *AccessConfig) blockStorageV3Client() (*gophercloud.ServiceClient, error) {
|
|
return openstack.NewBlockStorageV3(c.osClient, gophercloud.EndpointOpts{
|
|
Region: c.Region,
|
|
Availability: c.getEndpointType(),
|
|
})
|
|
}
|
|
|
|
func (c *AccessConfig) networkV2Client() (*gophercloud.ServiceClient, error) {
|
|
return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{
|
|
Region: c.Region,
|
|
Availability: c.getEndpointType(),
|
|
})
|
|
}
|
|
|
|
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
|
|
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
|
|
return gophercloud.AvailabilityInternal
|
|
}
|
|
if c.EndpointType == "admin" || c.EndpointType == "adminURL" {
|
|
return gophercloud.AvailabilityAdmin
|
|
}
|
|
return gophercloud.AvailabilityPublic
|
|
}
|
|
|
|
type DebugRoundTripper struct {
|
|
ui packersdk.Ui
|
|
rt http.RoundTripper
|
|
numReauthAttempts int
|
|
}
|
|
|
|
// RoundTrip performs a round-trip HTTP request and logs relevant information about it.
|
|
func (drt *DebugRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
defer func() {
|
|
if request.Body != nil {
|
|
request.Body.Close()
|
|
}
|
|
}()
|
|
|
|
var response *http.Response
|
|
var err error
|
|
|
|
response, err = drt.rt.RoundTrip(request)
|
|
if response == nil {
|
|
return nil, err
|
|
}
|
|
|
|
if response.StatusCode == http.StatusUnauthorized {
|
|
if drt.numReauthAttempts == 3 {
|
|
return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.")
|
|
}
|
|
drt.numReauthAttempts++
|
|
}
|
|
|
|
drt.DebugMessage(fmt.Sprintf("Request %s %s %d", request.Method, request.URL, response.StatusCode))
|
|
|
|
if response.StatusCode >= 400 {
|
|
buf := bytes.NewBuffer([]byte{})
|
|
body, _ := ioutil.ReadAll(io.TeeReader(response.Body, buf))
|
|
drt.DebugMessage(fmt.Sprintf("Response Error: %+v\n", string(body)))
|
|
bufWithClose := ioutil.NopCloser(buf)
|
|
response.Body = bufWithClose
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
func (drt *DebugRoundTripper) DebugMessage(message string) {
|
|
drt.ui.Message(fmt.Sprintf("[DEBUG] %s", message))
|
|
}
|