Skip to content

Commit

Permalink
storage: add Directive, Specification
Browse files Browse the repository at this point in the history
We introduce two new structures: Directive
and Specification. Directive contains user-
specified requirements, while Specification
contains both user-specified and charm-specified
requirements.

There is an associated ParseDirective function
that will be used by "juju deploy" and
"juju add-unit" later.
  • Loading branch information
axw committed Nov 18, 2014
1 parent 46cbb36 commit e7f85e6
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 21 deletions.
171 changes: 171 additions & 0 deletions storage/directive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storage

import (
"fmt"
"regexp"
"strconv"

"github.com/juju/errors"
"github.com/juju/utils"
)

const (
// ProviderSource identifies the environment's cloud provider
// storage service(s).
ProviderSource = "provider"

storageNameSnippet = "(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*)"
storageSourceSnippet = "(?:[a-z][a-z0-9]*)"
storageCountSnippet = "-?[0-9]+"
storageSizeSnippet = "-?[0-9]+(?:\\.[0-9]+)?[MGTP]?"
storageOptionsSnippet = ".*"
)

// ErrStorageSourceMissing is an error that is returned from ParseDirective
// if the source is unspecified.
var ErrStorageSourceMissing = fmt.Errorf("storage source missing")

var storageRE = regexp.MustCompile(
"^" +
"(?:(" + storageNameSnippet + ")=)?" +
"(?:(" + storageSourceSnippet + "):)?" +
"(?:(" + storageCountSnippet + ")x)?" +
"(" + storageSizeSnippet + ")?" +
"(" + storageOptionsSnippet + ")?" +
"$",
)

// Directive is a storage creation directive.
type Directive struct {
// Name is the name of the storage.
//
// Name is required.
Name string

// Source is the storage source (provider, ceph, ...).
//
// Source is required.
Source string

// Count is the number of instances of the store to create/attach.
//
// Count is optional. Count will default to 1 if a size is
// specified, otherwise it will default to 0.
Count int

// Size is the size of the storage in MiB.
//
// Size's optionality depends on the storage source. For some
// types of storage (e.g. an NFS share), it is not meaningful
// to specify a size; for others (e.g. EBS), it is necessary.
Size uint64

// Options is source-specific options for storage creation.
Options string
}

// ParseDirective attempts to parse the string and create a
// corresponding Directive structure.
//
// If a storage source is not specified, ParseDirective will
// return ErrStorageSourceMissing.
//
// The acceptable format for storage directives is:
// NAME=SOURCE:[[COUNTx]SIZE][,OPTIONS]
// where
// NAME is an identifier for storage instances; multiple
// instances may share the same storage name. NAME can be a
// string starting with a letter of the alphabet, followed
// by zero or more alpha-numeric characters.
//
// SOURCE identifies the storage source. SOURCE can be a
// string starting with a letter of the alphabet, followed
// by zero or more alpha-numeric characters optionally
// separated by hyphens.
//
// COUNT is a decimal number indicating how many instances
// of the storage to create. If count is unspecified and a
// size is specified, 1 is assumed.
//
// SIZE is a floating point number and optional multiplier from
// the set (M, G, T, P), which are all treated as powers of 1024.
//
// OPTIONS is the string remaining the colon (if any) that will
// be passed onto the storage source unmodified.
func ParseDirective(s string) (*Directive, error) {
match := storageRE.FindStringSubmatch(s)
if match == nil {
return nil, errors.Errorf("failed to parse storage %q", s)
}
if match[1] == "" {
return nil, errors.New("storage name missing")
}
if match[2] == "" {
return nil, ErrStorageSourceMissing
}

var size uint64
var count int
var err error
if match[4] != "" {
size, err = utils.ParseSize(match[4])
if err != nil {
return nil, errors.Annotate(err, "failed to parse size")
}
}
options := match[5]

if size > 0 {
// Don't bother parsing count unless we have a size too.
if count, err = parseStorageCount(match[3]); err != nil {
return nil, err
}

// Size was specified, so options must be preceded by a ",".
if options != "" {
if options[0] != ',' {
return nil, errors.Errorf(
"invalid trailing data %q: options must be preceded by ',' when size is specified",
options,
)
}
options = options[1:]
}
}

storage := Directive{
Name: match[1],
Source: match[2],
Count: count,
Size: size,
Options: options,
}
return &storage, nil
}

func parseStorageCount(count string) (int, error) {
if count == "" {
return 1, nil
}
n, err := strconv.Atoi(count)
if err != nil {
return -1, err
}
if n <= 0 {
return -1, errors.New("count must be a positive integer")
}
return n, nil
}

// MustParseDirective attempts to parse the string and create a
// corresponding Directive structure, panicking if an error occurs.
func MustParseDirective(s string) *Directive {
storage, err := ParseDirective(s)
if err != nil {
panic(err)
}
return storage
}
124 changes: 124 additions & 0 deletions storage/directive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storage_test

import (
gc "gopkg.in/check.v1"

"github.com/juju/juju/storage"
)

type DirectiveSuite struct{}

var _ = gc.Suite(&DirectiveSuite{})

func (s *DirectiveSuite) TestParseDirective(c *gc.C) {
parseStorageTests := []struct {
arg string
expectSource string
expectName string
expectCount int
expectSize uint64
expectPersistent bool
expectOptions string
err string
}{{
arg: "",
err: `storage name missing`,
}, {
arg: ":",
err: `storage name missing`,
}, {
arg: "1M",
err: "storage name missing",
}, {
arg: "ebs:1M",
err: "storage name missing",
}, {
arg: "name=1M",
err: "storage source missing",
}, {
arg: "name=source:1M",
expectName: "name",
expectSource: "source",
expectCount: 1,
expectSize: 1,
}, {
arg: "n-a-m-e=source:1M",
expectName: "n-a-m-e",
expectSource: "source",
expectCount: 1,
expectSize: 1,
}, {
arg: "name=source:1Msomejunk",
err: `invalid trailing data "somejunk": options must be preceded by ',' when size is specified`,
}, {
arg: "name=source:anyoldjunk",
expectName: "name",
expectSource: "source",
expectCount: 0,
expectSize: 0,
expectOptions: "anyoldjunk",
}, {
arg: "name=source:1M,",
expectName: "name",
expectSource: "source",
expectCount: 1,
expectSize: 1,
}, {
arg: "name=source:1M,whatever options that please me",
expectName: "name",
expectSource: "source",
expectCount: 1,
expectSize: 1,
expectOptions: "whatever options that please me",
}, {
arg: "n=s:1G",
expectName: "n",
expectSource: "s",
expectCount: 1,
expectSize: 1024,
}, {
arg: "n=s:0.5T",
expectName: "n",
expectSource: "s",
expectCount: 1,
expectSize: 1024 * 512,
}, {
arg: "n=s:3x0.125P",
expectName: "n",
expectSource: "s",
expectCount: 3,
expectSize: 1024 * 1024 * 128,
}, {
arg: "n=s:0x100M",
err: "count must be a positive integer",
}, {
arg: "n=s:-1x100M",
err: "count must be a positive integer",
}, {
arg: "n=s:-100M",
err: `failed to parse size: expected a non-negative number with optional multiplier suffix \(M/G/T/P\), got "-100M"`,
}}

for i, t := range parseStorageTests {
c.Logf("test %d: %q", i, t.arg)
p, err := storage.ParseDirective(t.arg)
if t.err != "" {
c.Check(err, gc.ErrorMatches, t.err)
c.Check(p, gc.IsNil)
} else {
if !c.Check(err, gc.IsNil) {
continue
}
c.Check(p, gc.DeepEquals, &storage.Directive{
Name: t.expectName,
Source: t.expectSource,
Count: t.expectCount,
Size: t.expectSize,
Options: t.expectOptions,
})
}
}
}
14 changes: 14 additions & 0 deletions storage/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storage_test

import (
stdtesting "testing"

gc "gopkg.in/check.v1"
)

func TestPackage(t *stdtesting.T) {
gc.TestingT(t)
}
25 changes: 25 additions & 0 deletions storage/sort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package storage

import "sort"

// SortBlockDevices sorts block devices by device name.
func SortBlockDevices(devices []BlockDevice) {
sort.Sort(byDeviceName(devices))
}

type byDeviceName []BlockDevice

func (b byDeviceName) Len() int {
return len(b)
}

func (b byDeviceName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

func (b byDeviceName) Less(i, j int) bool {
return b[i].DeviceName < b[j].DeviceName
}
25 changes: 25 additions & 0 deletions storage/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package storage

// Specification is a fully specified set of requirements for storage,
// derived from a Directive and a charm's storage metadata.
type Specification struct {
// Name is the name of the storage.
Name string

// Source is the storage source (provider, ceph, ...).
Source string

// Size is the size of the storage in MiB.
Size uint64

// Options is source-specific options for storage creation.
Options string

// ReadOnly indicates that the storage should be made read-only if
// possible.
ReadOnly bool

// Persistent indicates that the storage should be made persistent,
// beyond the lifetime of the entity it is attached to, if possible.
Persistent bool
}
21 changes: 0 additions & 21 deletions storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

package storage

import "sort"

// BlockDevice describes a block device (disk, logical volume, etc.)
type BlockDevice struct {
// DeviceName is the block device's OS-specific name (e.g. "sdb").
Expand All @@ -31,22 +29,3 @@ type BlockDevice struct {
// InUse indicates that the block device is in use (e.g. mounted).
InUse bool `yaml:"inuse"`
}

// SortBlockDevices sorts block devices by device name.
func SortBlockDevices(devices []BlockDevice) {
sort.Sort(byDeviceName(devices))
}

type byDeviceName []BlockDevice

func (b byDeviceName) Len() int {
return len(b)
}

func (b byDeviceName) Swap(i, j int) {
b[i], b[j] = b[j], b[i]
}

func (b byDeviceName) Less(i, j int) bool {
return b[i].DeviceName < b[j].DeviceName
}

0 comments on commit e7f85e6

Please sign in to comment.