github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/oci/provider.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package oci 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "net" 10 "os" 11 "strings" 12 13 "github.com/juju/clock" 14 "github.com/juju/errors" 15 "github.com/juju/jsonschema" 16 "github.com/juju/loggo" 17 "github.com/juju/schema" 18 ociCore "github.com/oracle/oci-go-sdk/core" 19 ociIdentity "github.com/oracle/oci-go-sdk/identity" 20 "gopkg.in/ini.v1" 21 "gopkg.in/juju/environschema.v1" 22 23 "github.com/juju/juju/cloud" 24 "github.com/juju/juju/core/instance" 25 "github.com/juju/juju/environs" 26 "github.com/juju/juju/environs/config" 27 "github.com/juju/juju/environs/context" 28 "github.com/juju/juju/provider/oci/common" 29 ) 30 31 var logger = loggo.GetLogger("juju.provider.oci") 32 33 // EnvironProvider type implements environs.EnvironProvider interface 34 type EnvironProvider struct{} 35 36 type environConfig struct { 37 *config.Config 38 attrs map[string]interface{} 39 } 40 41 var _ config.ConfigSchemaSource = (*EnvironProvider)(nil) 42 var _ environs.ProviderSchema = (*EnvironProvider)(nil) 43 44 // var cloudSchema = &jsonschema.Schema{ 45 // Type: []jsonschema.Type{jsonschema.ObjectType}, 46 // Required: []string{cloud.RegionsKey, cloud.AuthTypesKey}, 47 // Order: []string{cloud.RegionsKey, cloud.AuthTypesKey}, 48 // Properties: map[string]*jsonschema.Schema{ 49 // cloud.RegionsKey: { 50 // Type: []jsonschema.Type{jsonschema.ObjectType}, 51 // Singular: "region", 52 // Plural: "regions", 53 // AdditionalProperties: &jsonschema.Schema{ 54 // Type: []jsonschema.Type{jsonschema.ObjectType}, 55 // }, 56 // }, 57 // cloud.AuthTypesKey: { 58 // // don't need a prompt, since there's only one choice. 59 // Type: []jsonschema.Type{jsonschema.ArrayType}, 60 // Enum: []interface{}{[]string{string(cloud.HTTPSigAuthType)}}, 61 // }, 62 // }, 63 // } 64 65 var configSchema = environschema.Fields{ 66 "compartment-id": { 67 Description: "The OCID of the compartment in which juju has access to create resources.", 68 Type: environschema.Tstring, 69 }, 70 "address-space": { 71 Description: "The CIDR block to use when creating default subnets. The subnet must have at least a /16 size.", 72 Type: environschema.Tstring, 73 }, 74 } 75 76 var configDefaults = schema.Defaults{ 77 "compartment-id": "", 78 "address-space": DefaultAddressSpace, 79 } 80 81 var configFields = func() schema.Fields { 82 fs, _, err := configSchema.ValidationSchema() 83 if err != nil { 84 panic(err) 85 } 86 return fs 87 }() 88 89 // credentialSection holds the keys present in one section of the OCI 90 // config file, as created by the OCI command line. This is only used 91 // during credential detection 92 type credentialSection struct { 93 User string 94 Tenancy string 95 KeyFile string 96 PassPhrase string 97 Fingerprint string 98 Region string 99 } 100 101 var credentialSchema = map[cloud.AuthType]cloud.CredentialSchema{ 102 cloud.HTTPSigAuthType: { 103 { 104 "user", cloud.CredentialAttr{ 105 Description: "Username OCID", 106 }, 107 }, 108 { 109 "tenancy", cloud.CredentialAttr{ 110 Description: "Tenancy OCID", 111 }, 112 }, 113 { 114 "key", cloud.CredentialAttr{ 115 Description: "PEM encoded private key", 116 }, 117 }, 118 { 119 "pass-phrase", cloud.CredentialAttr{ 120 Description: "Passphrase used to unlock the key", 121 Hidden: true, 122 }, 123 }, 124 { 125 "fingerprint", cloud.CredentialAttr{ 126 Description: "Private key fingerprint", 127 }, 128 }, 129 { 130 "region", cloud.CredentialAttr{ 131 Description: "Region to log into", 132 }, 133 }, 134 }, 135 } 136 137 func (p EnvironProvider) newConfig(cfg *config.Config) (*environConfig, error) { 138 if cfg == nil { 139 return nil, errors.New("cannot set config on uninitialized env") 140 } 141 142 valid, err := p.Validate(cfg, nil) 143 if err != nil { 144 return nil, err 145 } 146 return &environConfig{valid, valid.UnknownAttrs()}, nil 147 } 148 149 func (c *environConfig) compartmentID() *string { 150 compartmentID := c.attrs["compartment-id"].(string) 151 if compartmentID == "" { 152 return nil 153 } 154 return &compartmentID 155 } 156 157 func (c *environConfig) addressSpace() *string { 158 addressSpace := c.attrs["address-space"].(string) 159 if addressSpace == "" { 160 addressSpace = DefaultAddressSpace 161 } 162 return &addressSpace 163 } 164 165 // Schema implements environs.ProviderSchema 166 func (o *EnvironProvider) Schema() environschema.Fields { 167 fields, err := config.Schema(configSchema) 168 if err != nil { 169 panic(err) 170 } 171 return fields 172 } 173 174 // ConfigSchema implements config.ConfigSchemaSource 175 func (o *EnvironProvider) ConfigSchema() schema.Fields { 176 return configFields 177 } 178 179 // ConfigDefaults implements config.ConfigSchemaSource 180 func (o *EnvironProvider) ConfigDefaults() schema.Defaults { 181 return configDefaults 182 } 183 184 // Version implements environs.EnvironProvider. 185 func (e EnvironProvider) Version() int { 186 return 1 187 } 188 189 // CloudSchema implements environs.EnvironProvider. 190 func (e EnvironProvider) CloudSchema() *jsonschema.Schema { 191 return nil 192 } 193 194 // Ping implements environs.EnvironProvider. 195 func (e *EnvironProvider) Ping(ctx context.ProviderCallContext, endpoint string) error { 196 return errors.NotImplementedf("Ping") 197 } 198 199 func validateCloudSpec(c environs.CloudSpec) error { 200 if err := c.Validate(); err != nil { 201 return errors.Trace(err) 202 } 203 if c.Credential == nil { 204 return errors.NotValidf("missing credential") 205 } 206 if authType := c.Credential.AuthType(); authType != cloud.HTTPSigAuthType { 207 return errors.NotSupportedf("%q auth-type", authType) 208 } 209 return nil 210 } 211 212 // PrepareConfig implements environs.EnvironProvider. 213 func (e EnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { 214 if err := validateCloudSpec(args.Cloud); err != nil { 215 return nil, errors.Annotate(err, "validating cloud spec") 216 } 217 // TODO(gsamfira): Set default block storage backend 218 return args.Config, nil 219 } 220 221 // Open implements environs.EnvironProvider. 222 func (e *EnvironProvider) Open(params environs.OpenParams) (environs.Environ, error) { 223 logger.Infof("opening model %q", params.Config.Name()) 224 225 if err := validateCloudSpec(params.Cloud); err != nil { 226 return nil, errors.Trace(err) 227 } 228 229 creds := params.Cloud.Credential.Attributes() 230 jujuConfig := common.JujuConfigProvider{ 231 Key: []byte(creds["key"]), 232 Fingerprint: creds["fingerprint"], 233 Passphrase: creds["pass-phrase"], 234 Tenancy: creds["tenancy"], 235 User: creds["user"], 236 OCIRegion: creds["region"], 237 } 238 provider, err := jujuConfig.Config() 239 if err != nil { 240 return nil, errors.Trace(err) 241 } 242 compute, err := ociCore.NewComputeClientWithConfigurationProvider(provider) 243 if err != nil { 244 return nil, errors.Trace(err) 245 } 246 247 networking, err := ociCore.NewVirtualNetworkClientWithConfigurationProvider(provider) 248 if err != nil { 249 return nil, errors.Trace(err) 250 } 251 252 storage, err := ociCore.NewBlockstorageClientWithConfigurationProvider(provider) 253 if err != nil { 254 return nil, errors.Trace(err) 255 } 256 257 identity, err := ociIdentity.NewIdentityClientWithConfigurationProvider(provider) 258 if err != nil { 259 return nil, errors.Trace(err) 260 } 261 262 env := &Environ{ 263 Compute: compute, 264 Networking: networking, 265 Storage: storage, 266 Firewall: networking, 267 Identity: identity, 268 ociConfig: provider, 269 clock: clock.WallClock, 270 p: e, 271 } 272 273 if err := env.SetConfig(params.Config); err != nil { 274 return nil, err 275 } 276 277 env.namespace, err = instance.NewNamespace(env.Config().UUID()) 278 279 cfg := env.ecfg() 280 if cfg.compartmentID() == nil { 281 return nil, errors.New("compartment-id may not be empty") 282 } 283 284 addressSpace := cfg.addressSpace() 285 if _, ipNET, err := net.ParseCIDR(*addressSpace); err == nil { 286 size, _ := ipNET.Mask.Size() 287 if size > 16 { 288 return nil, errors.Errorf("configured subnet (%q) is not large enough. Please use a prefix length in the range /8 to /16. Current prefix length is /%d", *addressSpace, size) 289 } 290 } else { 291 return nil, errors.Trace(err) 292 } 293 294 return env, nil 295 } 296 297 // CredentialSchemas implements environs.ProviderCredentials. 298 func (e EnvironProvider) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { 299 return credentialSchema 300 } 301 302 // DetectCredentials implements environs.ProviderCredentials. 303 // Configuration options for the OCI SDK are detailed here: 304 // https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm 305 func (e EnvironProvider) DetectCredentials() (*cloud.CloudCredential, error) { 306 result := cloud.CloudCredential{ 307 AuthCredentials: make(map[string]cloud.Credential), 308 } 309 cfg_file, err := ociConfigFile() 310 if err != nil { 311 if os.IsNotExist(errors.Cause(err)) { 312 return &result, nil 313 } 314 return nil, errors.Trace(err) 315 } 316 317 cfg, err := ini.LooseLoad(cfg_file) 318 if err != nil { 319 return nil, errors.Trace(err) 320 } 321 cfg.NameMapper = ini.TitleUnderscore 322 323 var defaultRegion string 324 325 for _, val := range cfg.SectionStrings() { 326 values := new(credentialSection) 327 if err := cfg.Section(val).MapTo(values); err != nil { 328 logger.Warningf("invalid value in section %s: %s", val, err) 329 continue 330 } 331 missingFields := []string{} 332 if values.User == "" { 333 missingFields = append(missingFields, "user") 334 } 335 336 if values.Tenancy == "" { 337 missingFields = append(missingFields, "tenancy") 338 } 339 340 if values.KeyFile == "" { 341 missingFields = append(missingFields, "key_file") 342 } 343 344 if values.Fingerprint == "" { 345 missingFields = append(missingFields, "fingerprint") 346 } 347 if values.Region == "" { 348 missingFields = append(missingFields, "region") 349 } 350 351 if len(missingFields) > 0 { 352 logger.Warningf("missing required field(s) in section %s: %s", val, strings.Join(missingFields, ", ")) 353 continue 354 } 355 356 pemFileContent, err := ioutil.ReadFile(values.KeyFile) 357 if err != nil { 358 return nil, errors.Trace(err) 359 } 360 if err := common.ValidateKey(pemFileContent, values.PassPhrase); err != nil { 361 logger.Warningf("failed to decrypt PEM %s using the configured pass phrase", values.KeyFile) 362 continue 363 } 364 365 if val == "DEFAULT" { 366 defaultRegion = values.Region 367 } 368 httpSigCreds := cloud.NewCredential( 369 cloud.HTTPSigAuthType, 370 map[string]string{ 371 "user": values.User, 372 "tenancy": values.Tenancy, 373 "key": string(pemFileContent), 374 "pass-phrase": values.PassPhrase, 375 "fingerprint": values.Fingerprint, 376 "region": values.Region, 377 }, 378 ) 379 httpSigCreds.Label = fmt.Sprintf("OCI credential %q", val) 380 result.AuthCredentials[val] = httpSigCreds 381 } 382 if len(result.AuthCredentials) == 0 { 383 return nil, errors.NotFoundf("OCI credentials") 384 } 385 result.DefaultRegion = defaultRegion 386 return &result, nil 387 } 388 389 // FinalizeCredential implements environs.ProviderCredentials. 390 func (e EnvironProvider) FinalizeCredential( 391 ctx environs.FinalizeCredentialContext, 392 params environs.FinalizeCredentialParams) (*cloud.Credential, error) { 393 394 return ¶ms.Credential, nil 395 } 396 397 // Validate implements config.Validator. 398 func (e EnvironProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { 399 if err := config.Validate(cfg, old); err != nil { 400 return nil, err 401 } 402 newAttrs, err := cfg.ValidateUnknownAttrs( 403 configFields, configDefaults, 404 ) 405 if err != nil { 406 return nil, err 407 } 408 409 return cfg.Apply(newAttrs) 410 }