Skip to content

Commit

Permalink
Add EFS storage driver
Browse files Browse the repository at this point in the history
  • Loading branch information
mhrabovcin committed Aug 12, 2016
1 parent e4cdd05 commit 4c8a858
Show file tree
Hide file tree
Showing 15 changed files with 1,952 additions and 0 deletions.
95 changes: 95 additions & 0 deletions .docs/user-guide/storage-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,98 @@ libstorage:
- Snapshot and create volume from volume functionality is not available yet
with this driver.
- The driver supports VirtualBox 5.0.10+

## AWS EFS
The AWS EFS driver registers a storage driver named `efs` with the
`libStorage` driver manager and is used to connect and manage AWS Elastic File
Systems.

### Requirements

* AWS account
* VPC - EFS can be accessed within VPC
* AWS Credentials

### Configuration
The following is an example with all possible fields configured. For a running
example see the `Examples` section.

```yaml
efs:
accessKey: XXXXXXXXXX
secretKey: XXXXXXXXXX
securityGroups: sg-XXXXXXX,sg-XXXXXX0,sg-XXXXXX1
region: us-east-1
tag: test
```

#### Configuration Notes
- The `accessKey` and `secretKey` configuration parameters are optional and should
be used when explicit AWS credentials configuration needs to be provided. EFS driver
uses official golang AWS SDK library and supports all other ways of providing
access credentials, like environment variables or instance profile IAM permissions.
- `region` represents AWS region where should be EFS provisioned. See official AWS
documentation for list of supported regions.
- `securityGroups` list of security groups attached to `MountPoint` instances.
If no security groups are provided the default VPC security group is used.
- `tag` is used to partition multiple services within single AWS account and is
used as prefix for EFS names in format `[tagprefix]/volumeName`.

For information on the equivalent environment variable and CLI flag names
please see the section on how non top-level configuration properties are
[transformed](./config.md#configuration-properties).

### Runtime Behavior

AWS EFS storage driver creates one EFS FileSystem per volume and provides root
of the filesystem as NFS mount point. Volumes aren't attached to instances
directly but rather exposed to each subnet by creating `MountPoint` in each VPC
subnet. When detaching volume from instance no action is taken as there isn't
good way to figure out if there are other instances in same subnet using
`MountPoint` that is being detached. There is no charge for `MountPoint`
so they are removed only once whole volume is deleted.

By default all EFS instances are provisioned as `generalPurpose` performance mode.
`maxIO` EFS type can be provisioned by providing `maxIO` flag as `volumetype`.

Its possible to mount same volume to multiple container on a single EC2 instance
as well as use single volume across multiple EC2 instances at the same time.

**NOTE**: Each EFS FileSystem can be accessed only from single VPC at the time.

### Activating the Driver
To activate the AWS EFS driver please follow the instructions for
[activating storage drivers](./config.md#storage-drivers),
using `efs` as the driver name.

### Troubleshooting
- Make sure that AWS credentials (user or role) has following AWS permissions on
`libStorage` server instance that will be making calls to AWS API:
- `elasticfilesystem:CreateFileSystem`
- `elasticfilesystem:CreateMountTarget`
- `ec2:DescribeSubnets`
- `ec2:DescribeNetworkInterfaces`
- `ec2:CreateNetworkInterface`
- `elasticfilesystem:CreateTags`
- `elasticfilesystem:DeleteFileSystem`
- `elasticfilesystem:DeleteMountTarget`
- `ec2:DeleteNetworkInterface`
- `elasticfilesystem:DescribeFileSystems`
- `elasticfilesystem:DescribeMountTargets`

### Examples
Below is a working `config.yml` file that works with AWS EFS.

```yaml
libstorage:
server:
services:
efs:
driver: efs
efs:
accessKey: XXXXXXXXXX
secretKey: XXXXXXXXXX
securityGroups: sg-XXXXXXX,sg-XXXXXX0,sg-XXXXXX1
region: us-east-1
tag: test
```
24 changes: 24 additions & 0 deletions drivers/storage/efs/efs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package efs

import (
"github.com/akutz/gofig"
)

const (
// Name is the provider's name.
Name = "efs"
)

func init() {
registerConfig()
}

func registerConfig() {
r := gofig.NewRegistration("EFS")
r.Key(gofig.String, "", "", "", "efs.accessKey")
r.Key(gofig.String, "", "", "", "efs.secretKey")
r.Key(gofig.String, "", "", "Comma separated security group ids", "efs.securityGroups")
r.Key(gofig.String, "", "", "AWS region", "efs.region")
r.Key(gofig.String, "", "", "Tag prefix for EFS naming", "efs.tag")
gofig.Register(r)
}
196 changes: 196 additions & 0 deletions drivers/storage/efs/executor/efs_executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package executor

import (
"bufio"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"

"github.com/akutz/gofig"
"github.com/akutz/goof"

"github.com/emccode/libstorage/api/registry"
"github.com/emccode/libstorage/api/types"
"github.com/emccode/libstorage/drivers/storage/efs"
)

// driver is the storage executor for the efs storage driver.
type driver struct {
config gofig.Config
subnetResolver SubnetResolver
}

const (
idDelimiter = "/"
mountinfoFormat = "%d %d %d:%d %s %s %s %s"
)

func init() {
registry.RegisterStorageExecutor(efs.Name, newDriver)
}

func newDriver() types.StorageExecutor {
return &driver{
subnetResolver: NewAwsVpcSubnetResolver(),
}
}

func (d *driver) Init(ctx types.Context, config gofig.Config) error {
d.config = config
return nil
}

func (d *driver) Name() string {
return efs.Name
}

// InstanceID returns the local instance ID for the test
func InstanceID() (*types.InstanceID, error) {
return newDriver().InstanceID(nil, nil)
}

// InstanceID returns the aws instance configuration
func (d *driver) InstanceID(
ctx types.Context,
opts types.Store) (*types.InstanceID, error) {

subnetID, err := d.subnetResolver.ResolveSubnet()
if err != nil {
return nil, goof.WithError("no ec2metadata subnet id", err)
}

iid := &types.InstanceID{Driver: efs.Name}
if err := iid.MarshalMetadata(subnetID); err != nil {
return nil, err
}

return iid, nil
}

func (d *driver) NextDevice(
ctx types.Context,
opts types.Store) (string, error) {
return "", types.ErrNotImplemented
}

func (d *driver) LocalDevices(
ctx types.Context,
opts *types.LocalDevicesOpts) (*types.LocalDevices, error) {

mtt, err := parseMountTable()
if err != nil {
return nil, err
}

idmnt := make(map[string]string)
for _, mt := range mtt {
idmnt[mt.Source] = mt.MountPoint
}

return &types.LocalDevices{
Driver: efs.Name,
DeviceMap: idmnt,
}, nil
}

func parseMountTable() ([]*types.MountInfo, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return nil, err
}
defer f.Close()

return parseInfoFile(f)
}

func parseInfoFile(r io.Reader) ([]*types.MountInfo, error) {
var (
s = bufio.NewScanner(r)
out = []*types.MountInfo{}
)

for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}

var (
p = &types.MountInfo{}
text = s.Text()
optionalFields string
)

if _, err := fmt.Sscanf(text, mountinfoFormat,
&p.ID, &p.Parent, &p.Major, &p.Minor,
&p.Root, &p.MountPoint, &p.Opts, &optionalFields); err != nil {
return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err)
}
// Safe as mountinfo encodes mountpoints with spaces as \040.
index := strings.Index(text, " - ")
postSeparatorFields := strings.Fields(text[index+3:])
if len(postSeparatorFields) < 3 {
return nil, fmt.Errorf("Error found less than 3 fields post '-' in %q", text)
}

if optionalFields != "-" {
p.Optional = optionalFields
}

p.FSType = postSeparatorFields[0]
p.Source = postSeparatorFields[1]
p.VFSOpts = strings.Join(postSeparatorFields[2:], " ")
out = append(out, p)
}
return out, nil
}

// SubnetResolver defines interface that can resolve subnet from environment
type SubnetResolver interface {
ResolveSubnet() (string, error)
}

// AwsVpcSubnetResolver is thin interface that resolves instance subnet from
// ec2metadata service. This helper is used instead of bringing AWS SDK to
// executor on purpose to keep executor dependencies minimal.
type AwsVpcSubnetResolver struct {
ec2MetadataIPAddress string
}

// ResolveSubnet determines VPC subnet id on running AWS instance
func (r *AwsVpcSubnetResolver) ResolveSubnet() (string, error) {
resp, err := http.Get(r.getURL("mac"))
if err != nil {
return "", err
}
mac, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}

resp, err = http.Get(r.getURL(fmt.Sprintf("network/interfaces/macs/%s/subnet-id", mac)))
if err != nil {
return "", err
}
subnetID, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}

return string(subnetID), nil
}

func (r *AwsVpcSubnetResolver) getURL(path string) string {
return fmt.Sprintf("http://%s/latest/meta-data/%s", r.ec2MetadataIPAddress, path)
}

// NewAwsVpcSubnetResolver creates AwsVpcSubnetResolver for default AWS endpoint
func NewAwsVpcSubnetResolver() *AwsVpcSubnetResolver {
return &AwsVpcSubnetResolver{
ec2MetadataIPAddress: "169.254.169.254",
}
}
Loading

0 comments on commit 4c8a858

Please sign in to comment.