cli: add basic support for constellation create on OpenStack (#1283)

* image: support OpenStack image build / upload

* cli: add OpenStack terraform template

* config: add OpenStack as CSP

* versionsapi: add OpenStack as CSP

* cli: add OpenStack as provider for `config generate` and `create`

* disk-mapper: add basic support for boot on OpenStack

* debugd: add placeholder for OpenStack

* image: fix config file sourcing for image upload
This commit is contained in:
Malte Poll 2023-02-27 18:19:52 +01:00 committed by GitHub
parent b013a7ab32
commit b79f7d0c8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 954 additions and 79 deletions

View file

@ -25,6 +25,8 @@ const (
Azure
// GCP is Google Compute Platform.
GCP
// OpenStack is an open standard cloud computing platform.
OpenStack
// QEMU for a local emulated installation.
QEMU
)
@ -69,6 +71,8 @@ func FromString(s string) Provider {
return Azure
case "gcp":
return GCP
case "openstack":
return OpenStack
case "qemu":
return QEMU
default:

View file

@ -35,6 +35,10 @@ func TestMarshalJSON(t *testing.T) {
input: GCP,
want: []byte("\"GCP\""),
},
"openstack": {
input: OpenStack,
want: []byte("\"OpenStack\""),
},
"qemu": {
input: QEMU,
want: []byte("\"QEMU\""),
@ -79,6 +83,10 @@ func TestUnmarshalJSON(t *testing.T) {
input: []byte("\"gcp\""),
want: GCP,
},
"openstack": {
input: []byte("\"openstack\""),
want: OpenStack,
},
"qemu": {
input: []byte("\"qemu\""),
want: QEMU,
@ -123,6 +131,10 @@ func TestMarshalYAML(t *testing.T) {
input: GCP,
want: []byte("GCP\n"),
},
"openstack": {
input: OpenStack,
want: []byte("OpenStack\n"),
},
"qemu": {
input: QEMU,
want: []byte("QEMU\n"),
@ -167,6 +179,10 @@ func TestUnmarshalYAML(t *testing.T) {
input: []byte("gcp\n"),
want: GCP,
},
"openstack": {
input: []byte("openstack\n"),
want: OpenStack,
},
"qemu": {
input: []byte("qemu\n"),
want: QEMU,
@ -215,6 +231,10 @@ func TestFromString(t *testing.T) {
input: "gcp",
want: GCP,
},
"openstack": {
input: "openstack",
want: OpenStack,
},
"qemu": {
input: "qemu",
want: QEMU,

View file

@ -12,12 +12,13 @@ func _() {
_ = x[AWS-1]
_ = x[Azure-2]
_ = x[GCP-3]
_ = x[QEMU-4]
_ = x[OpenStack-4]
_ = x[QEMU-5]
}
const _Provider_name = "UnknownAWSAzureGCPQEMU"
const _Provider_name = "UnknownAWSAzureGCPOpenStackQEMU"
var _Provider_index = [...]uint8{0, 7, 10, 15, 18, 22}
var _Provider_index = [...]uint8{0, 7, 10, 15, 18, 27, 31}
func (i Provider) String() string {
if i >= Provider(len(_Provider_index)-1) {

View file

@ -114,6 +114,9 @@ type ProviderConfig struct {
// Configuration for Google Cloud as provider.
GCP *GCPConfig `yaml:"gcp,omitempty" validate:"omitempty,dive"`
// description: |
// Configuration for OpenStack as provider.
OpenStack *OpenStackConfig `yaml:"openstack,omitempty" validate:"omitempty,dive"`
// description: |
// Configuration for QEMU as provider.
QEMU *QEMUConfig `yaml:"qemu,omitempty" validate:"omitempty,dive"`
}
@ -220,6 +223,25 @@ type GCPConfig struct {
Measurements Measurements `yaml:"measurements" validate:"required,no_placeholders"`
}
// OpenStackConfig holds config information for OpenStack based Constellation deployments.
type OpenStackConfig struct {
// description: |
// OpenStack cloud name to select from "clouds.yaml". Only required if config file for OpenStack is used. Fallback authentication uses environment variables. For details see: https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html.
Cloud string `yaml:"cloud"`
// description: |
// Availability zone to place the VMs in. For details see: https://docs.openstack.org/nova/latest/admin/availability-zones.html
AvailabilityZone string `yaml:"availabilityZone" validate:"required"`
// description: |
// Flavor ID (machine type) to use for the VMs. For details see: https://docs.openstack.org/nova/latest/admin/flavors.html
FlavorID string `yaml:"flavorID" validate:"required"`
// description: |
// Floating IP pool to use for the VMs. For details see: https://docs.openstack.org/ocata/user-guide/cli-manage-ip-addresses.html
FloatingIPPoolID string `yaml:"floatingIPPoolID" validate:"required"`
// description: |
// If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack.
DirectDownload *bool `yaml:"directDownload" validate:"required"`
}
// QEMUConfig holds config information for QEMU based Constellation deployments.
type QEMUConfig struct {
// description: |
@ -295,6 +317,9 @@ func Default() *Config {
DeployCSIDriver: func() *bool { b := true; return &b }(),
Measurements: measurements.DefaultsFor(cloudprovider.GCP),
},
OpenStack: &OpenStackConfig{
DirectDownload: func() *bool { b := true; return &b }(),
},
QEMU: &QEMUConfig{
ImageFormat: "raw",
VCPUs: 2,
@ -393,6 +418,8 @@ func (c *Config) RemoveProviderExcept(provider cloudprovider.Provider) {
c.Provider.Azure = currentProviderConfigs.Azure
case cloudprovider.GCP:
c.Provider.GCP = currentProviderConfigs.GCP
case cloudprovider.OpenStack:
c.Provider.OpenStack = currentProviderConfigs.OpenStack
case cloudprovider.QEMU:
c.Provider.QEMU = currentProviderConfigs.QEMU
default:
@ -429,6 +456,9 @@ func (c *Config) GetProvider() cloudprovider.Provider {
if c.Provider.GCP != nil {
return cloudprovider.GCP
}
if c.Provider.OpenStack != nil {
return cloudprovider.OpenStack
}
if c.Provider.QEMU != nil {
return cloudprovider.QEMU
}

View file

@ -11,13 +11,14 @@ import (
)
var (
ConfigDoc encoder.Doc
UpgradeConfigDoc encoder.Doc
ProviderConfigDoc encoder.Doc
AWSConfigDoc encoder.Doc
AzureConfigDoc encoder.Doc
GCPConfigDoc encoder.Doc
QEMUConfigDoc encoder.Doc
ConfigDoc encoder.Doc
UpgradeConfigDoc encoder.Doc
ProviderConfigDoc encoder.Doc
AWSConfigDoc encoder.Doc
AzureConfigDoc encoder.Doc
GCPConfigDoc encoder.Doc
OpenStackConfigDoc encoder.Doc
QEMUConfigDoc encoder.Doc
)
func init() {
@ -110,7 +111,7 @@ func init() {
FieldName: "provider",
},
}
ProviderConfigDoc.Fields = make([]encoder.Doc, 4)
ProviderConfigDoc.Fields = make([]encoder.Doc, 5)
ProviderConfigDoc.Fields[0].Name = "aws"
ProviderConfigDoc.Fields[0].Type = "AWSConfig"
ProviderConfigDoc.Fields[0].Note = ""
@ -126,11 +127,16 @@ func init() {
ProviderConfigDoc.Fields[2].Note = ""
ProviderConfigDoc.Fields[2].Description = "Configuration for Google Cloud as provider."
ProviderConfigDoc.Fields[2].Comments[encoder.LineComment] = "Configuration for Google Cloud as provider."
ProviderConfigDoc.Fields[3].Name = "qemu"
ProviderConfigDoc.Fields[3].Type = "QEMUConfig"
ProviderConfigDoc.Fields[3].Name = "openstack"
ProviderConfigDoc.Fields[3].Type = "OpenStackConfig"
ProviderConfigDoc.Fields[3].Note = ""
ProviderConfigDoc.Fields[3].Description = "Configuration for QEMU as provider."
ProviderConfigDoc.Fields[3].Comments[encoder.LineComment] = "Configuration for QEMU as provider."
ProviderConfigDoc.Fields[3].Description = "Configuration for OpenStack as provider."
ProviderConfigDoc.Fields[3].Comments[encoder.LineComment] = "Configuration for OpenStack as provider."
ProviderConfigDoc.Fields[4].Name = "qemu"
ProviderConfigDoc.Fields[4].Type = "QEMUConfig"
ProviderConfigDoc.Fields[4].Note = ""
ProviderConfigDoc.Fields[4].Description = "Configuration for QEMU as provider."
ProviderConfigDoc.Fields[4].Comments[encoder.LineComment] = "Configuration for QEMU as provider."
AWSConfigDoc.Type = "AWSConfig"
AWSConfigDoc.Comments[encoder.LineComment] = "AWSConfig are AWS specific configuration values used by the CLI."
@ -315,6 +321,42 @@ func init() {
GCPConfigDoc.Fields[7].Description = "Expected confidential VM measurements."
GCPConfigDoc.Fields[7].Comments[encoder.LineComment] = "Expected confidential VM measurements."
OpenStackConfigDoc.Type = "OpenStackConfig"
OpenStackConfigDoc.Comments[encoder.LineComment] = "OpenStackConfig holds config information for OpenStack based Constellation deployments."
OpenStackConfigDoc.Description = "OpenStackConfig holds config information for OpenStack based Constellation deployments."
OpenStackConfigDoc.AppearsIn = []encoder.Appearance{
{
TypeName: "ProviderConfig",
FieldName: "openstack",
},
}
OpenStackConfigDoc.Fields = make([]encoder.Doc, 5)
OpenStackConfigDoc.Fields[0].Name = "cloud"
OpenStackConfigDoc.Fields[0].Type = "string"
OpenStackConfigDoc.Fields[0].Note = ""
OpenStackConfigDoc.Fields[0].Description = "OpenStack cloud name to select from \"clouds.yaml\". Only required if config file for OpenStack is used. Fallback authentication uses environment variables. For details see: https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html."
OpenStackConfigDoc.Fields[0].Comments[encoder.LineComment] = "OpenStack cloud name to select from \"clouds.yaml\". Only required if config file for OpenStack is used. Fallback authentication uses environment variables. For details see: https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html."
OpenStackConfigDoc.Fields[1].Name = "availabilityZone"
OpenStackConfigDoc.Fields[1].Type = "string"
OpenStackConfigDoc.Fields[1].Note = ""
OpenStackConfigDoc.Fields[1].Description = "Availability zone to place the VMs in. For details see: https://docs.openstack.org/nova/latest/admin/availability-zones.html"
OpenStackConfigDoc.Fields[1].Comments[encoder.LineComment] = "Availability zone to place the VMs in. For details see: https://docs.openstack.org/nova/latest/admin/availability-zones.html"
OpenStackConfigDoc.Fields[2].Name = "flavorID"
OpenStackConfigDoc.Fields[2].Type = "string"
OpenStackConfigDoc.Fields[2].Note = ""
OpenStackConfigDoc.Fields[2].Description = "Flavor ID (machine type) to use for the VMs. For details see: https://docs.openstack.org/nova/latest/admin/flavors.html"
OpenStackConfigDoc.Fields[2].Comments[encoder.LineComment] = "Flavor ID (machine type) to use for the VMs. For details see: https://docs.openstack.org/nova/latest/admin/flavors.html"
OpenStackConfigDoc.Fields[3].Name = "floatingIPPoolID"
OpenStackConfigDoc.Fields[3].Type = "string"
OpenStackConfigDoc.Fields[3].Note = ""
OpenStackConfigDoc.Fields[3].Description = "Floating IP pool to use for the VMs. For details see: https://docs.openstack.org/ocata/user-guide/cli-manage-ip-addresses.html"
OpenStackConfigDoc.Fields[3].Comments[encoder.LineComment] = "Floating IP pool to use for the VMs. For details see: https://docs.openstack.org/ocata/user-guide/cli-manage-ip-addresses.html"
OpenStackConfigDoc.Fields[4].Name = "directDownload"
OpenStackConfigDoc.Fields[4].Type = "bool"
OpenStackConfigDoc.Fields[4].Note = ""
OpenStackConfigDoc.Fields[4].Description = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack."
OpenStackConfigDoc.Fields[4].Comments[encoder.LineComment] = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack."
QEMUConfigDoc.Type = "QEMUConfig"
QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."
QEMUConfigDoc.Description = "QEMUConfig holds config information for QEMU based Constellation deployments."
@ -396,6 +438,10 @@ func (_ GCPConfig) Doc() *encoder.Doc {
return &GCPConfigDoc
}
func (_ OpenStackConfig) Doc() *encoder.Doc {
return &OpenStackConfigDoc
}
func (_ QEMUConfig) Doc() *encoder.Doc {
return &QEMUConfigDoc
}
@ -412,6 +458,7 @@ func GetConfigurationDoc() *encoder.FileDoc {
&AWSConfigDoc,
&AzureConfigDoc,
&GCPConfigDoc,
&OpenStackConfigDoc,
&QEMUConfigDoc,
},
}

View file

@ -184,7 +184,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
}
func TestValidate(t *testing.T) {
const defaultErrCount = 21 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const defaultErrCount = 24 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const azErrCount = 9
const gcpErrCount = 6

View file

@ -130,6 +130,9 @@ func validateProvider(sl validator.StructLevel) {
if provider.GCP != nil {
providerCount++
}
if provider.OpenStack != nil {
providerCount++
}
if provider.QEMU != nil {
providerCount++
}
@ -163,7 +166,7 @@ func translateGCPInstanceTypeError(ut ut.Translator, fe validator.FieldError) st
// Validation translation functions for Provider errors.
func registerNoProviderError(ut ut.Translator) error {
return ut.Add("no_provider", "{0}: No provider has been defined (requires either Azure, GCP or QEMU)", true)
return ut.Add("no_provider", "{0}: No provider has been defined (requires either Azure, GCP, OpenStack or QEMU)", true)
}
func translateNoProviderError(ut ut.Translator, fe validator.FieldError) string {

View file

@ -30,6 +30,8 @@ type ImageInfo struct {
Azure map[string]string `json:"azure,omitempty"`
// GCP is a map of image types to GCP image IDs.
GCP map[string]string `json:"gcp,omitempty"`
// OpenStack is a map of image types to OpenStack image IDs.
OpenStack map[string]string `json:"openstack,omitempty"`
// QEMU is a map of image types to QEMU image URLs.
QEMU map[string]string `json:"qemu,omitempty"`
}
@ -78,6 +80,9 @@ func (i ImageInfo) ValidateRequest() error {
if len(i.GCP) != 0 {
retErr = errors.Join(retErr, errors.New("GCP map must be empty for request"))
}
if len(i.OpenStack) != 0 {
retErr = errors.Join(retErr, errors.New("OpenStack map must be empty for request"))
}
if len(i.QEMU) != 0 {
retErr = errors.Join(retErr, errors.New("QEMU map must be empty for request"))
}
@ -97,17 +102,14 @@ func (i ImageInfo) Validate() error {
if !semver.IsValid(i.Version) {
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a valid semver", i.Version))
}
if len(i.AWS) == 0 {
retErr = errors.Join(retErr, errors.New("AWS map must not be empty"))
}
if len(i.Azure) == 0 {
retErr = errors.Join(retErr, errors.New("Azure map must not be empty"))
}
if len(i.GCP) == 0 {
retErr = errors.Join(retErr, errors.New("GCP map must not be empty"))
}
if len(i.QEMU) == 0 {
retErr = errors.Join(retErr, errors.New("QEMU map must not be empty"))
var providers int
providers += len(i.AWS)
providers += len(i.Azure)
providers += len(i.GCP)
providers += len(i.OpenStack)
providers += len(i.QEMU)
if providers == 0 {
retErr = errors.Join(retErr, errors.New("one or more providers must be specified"))
}
return retErr

View file

@ -129,47 +129,11 @@ func TestImageInfoValidate(t *testing.T) {
},
wantErr: true,
},
"invalid aws": {
"no provider": {
info: ImageInfo{
Ref: "test-ref",
Stream: "nightly",
Version: "v1.0.0",
GCP: map[string]string{"key": "value", "key2": "value2"},
Azure: map[string]string{"key": "value", "key2": "value2"},
QEMU: map[string]string{"key": "value", "key2": "value2"},
},
wantErr: true,
},
"invalid gcp": {
info: ImageInfo{
Ref: "test-ref",
Stream: "nightly",
Version: "v1.0.0",
AWS: map[string]string{"key": "value", "key2": "value2"},
Azure: map[string]string{"key": "value", "key2": "value2"},
QEMU: map[string]string{"key": "value", "key2": "value2"},
},
wantErr: true,
},
"invalid azure": {
info: ImageInfo{
Ref: "test-ref",
Stream: "nightly",
Version: "v1.0.0",
AWS: map[string]string{"key": "value", "key2": "value2"},
GCP: map[string]string{"key": "value", "key2": "value2"},
QEMU: map[string]string{"key": "value", "key2": "value2"},
},
wantErr: true,
},
"invalid qemu": {
info: ImageInfo{
Ref: "test-ref",
Stream: "nightly",
Version: "v1.0.0",
AWS: map[string]string{"key": "value", "key2": "value2"},
GCP: map[string]string{"key": "value", "key2": "value2"},
Azure: map[string]string{"key": "value", "key2": "value2"},
},
wantErr: true,
},
@ -269,6 +233,15 @@ func TestImageInfoValidateRequest(t *testing.T) {
},
wantErr: true,
},
"invalid openstack": {
info: ImageInfo{
Ref: "test-ref",
Stream: "nightly",
Version: "v1.0.0",
OpenStack: map[string]string{"key": "value", "key2": "value2"},
},
wantErr: true,
},
"multiple errors": {
info: ImageInfo{
Ref: "",