Skip to content

Commit

Permalink
Add EFS storage driver
Browse files Browse the repository at this point in the history
  • Loading branch information
mhrabovcin authored and Martin Hrabovcin committed Jul 14, 2016
1 parent e4cdd05 commit 314b734
Show file tree
Hide file tree
Showing 9 changed files with 1,176 additions and 0 deletions.
90 changes: 90 additions & 0 deletions .docs/user-guide/storage-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,93 @@ 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`.

**NOTE**: Each EFS FileSystem can be accessed only from single VPC by 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:
- `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)
}
157 changes: 157 additions & 0 deletions drivers/storage/efs/executor/efs_executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package executor

import (
"bufio"
"fmt"
"io"
"os"
"strings"

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

"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"

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

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

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

func newDriver() types.StorageExecutor {
return &driver{}
}

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) {

svc := ec2metadata.New(session.New())
if !svc.Available() {
return nil, goof.New("EC2Metadata service not available")
}

mac, err := svc.GetMetadata("mac")
if err != nil {
return nil, goof.WithError("no ec2metadata mac address", err)
}

subnetID, err := svc.GetMetadata(fmt.Sprintf("network/interfaces/macs/%s/subnet-id", mac))
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
}

// TODO(mhrabovcin): Filter out only AWS NFS mounts?
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
}
Loading

0 comments on commit 314b734

Please sign in to comment.