Browse code

Security allocator and repair logic

Clayton Coleman authored on 2015/06/01 10:34:56
Showing 11 changed files
... ...
@@ -29,6 +29,8 @@ import (
29 29
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
30 30
 	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
31 31
 	kmaster "github.com/GoogleCloudPlatform/kubernetes/pkg/master"
32
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
33
+	etcdallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator/etcd"
32 34
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
33 35
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
34 36
 
... ...
@@ -85,6 +87,10 @@ import (
85 85
 	routeregistry "github.com/openshift/origin/pkg/route/registry/route"
86 86
 	clusternetworketcd "github.com/openshift/origin/pkg/sdn/registry/clusternetwork/etcd"
87 87
 	hostsubnetetcd "github.com/openshift/origin/pkg/sdn/registry/hostsubnet/etcd"
88
+	securitycontroller "github.com/openshift/origin/pkg/security/controller"
89
+	"github.com/openshift/origin/pkg/security/mcs"
90
+	"github.com/openshift/origin/pkg/security/uid"
91
+	"github.com/openshift/origin/pkg/security/uidallocator"
88 92
 	"github.com/openshift/origin/pkg/service"
89 93
 	templateregistry "github.com/openshift/origin/pkg/template/registry"
90 94
 	templateetcd "github.com/openshift/origin/pkg/template/registry/etcd"
... ...
@@ -992,6 +998,40 @@ func (c *MasterConfig) RunImageImportController() {
992 992
 	controller.Run()
993 993
 }
994 994
 
995
+func (c *MasterConfig) RunSecurityAllocationController() {
996
+	uidRange, err := uid.ParseRange("1000000000-1999999999/10000") // provide 100k uid blocks
997
+	if err != nil {
998
+		glog.Fatalf("Unable to describe UID range: %v", err)
999
+	}
1000
+	var etcdAlloc *etcdallocator.Etcd
1001
+	uidAllocator := uidallocator.New(uidRange, func(max int, rangeSpec string) allocator.Interface {
1002
+		mem := allocator.NewContiguousAllocationMap(max, rangeSpec)
1003
+		etcdAlloc = etcdallocator.NewEtcd(mem, "/ranges/uids", "uidallocation", c.EtcdHelper)
1004
+		return etcdAlloc
1005
+	})
1006
+	mcsRange, err := mcs.ParseRange("/2") // use two labels
1007
+	if err != nil {
1008
+		glog.Fatalf("Unable to describe MCS category range: %v", err)
1009
+	}
1010
+
1011
+	kclient := c.PrivilegedLoopbackKubernetesClient
1012
+
1013
+	repair := securitycontroller.NewRepair(time.Minute, kclient.Namespaces(), uidRange, etcdAlloc)
1014
+	if err := repair.RunOnce(); err != nil {
1015
+		// TODO: v scary, may need to use direct etcd calls?
1016
+		glog.Fatalf("Unable to initialize namespaces: %v", err)
1017
+	}
1018
+
1019
+	factory := securitycontroller.AllocationFactory{
1020
+		UIDAllocator: uidAllocator,
1021
+		MCSAllocator: securitycontroller.DefaultMCSAllocation(uidRange, mcsRange, 5), // provide 5 labels per namespace
1022
+		Client:       kclient.Namespaces(),
1023
+		// TODO: reuse namespace cache
1024
+	}
1025
+	controller := factory.Create()
1026
+	controller.Run()
1027
+}
1028
+
995 1029
 // ensureCORSAllowedOrigins takes a string list of origins and attempts to covert them to CORS origin
996 1030
 // regexes, or exits if it cannot.
997 1031
 func (c *MasterConfig) ensureCORSAllowedOrigins() []*regexp.Regexp {
... ...
@@ -399,6 +399,7 @@ func StartMaster(openshiftMasterConfig *configapi.MasterConfig) error {
399 399
 	openshiftConfig.RunImageImportController()
400 400
 	openshiftConfig.RunOriginNamespaceController()
401 401
 	openshiftConfig.RunProjectAuthorizationCache()
402
+	openshiftConfig.RunSecurityAllocationController()
402 403
 	openshiftConfig.RunServiceAccountsController()
403 404
 	openshiftConfig.RunServiceAccountTokensController()
404 405
 	openshiftConfig.RunServiceAccountPullSecretsControllers()
405 406
new file mode 100644
... ...
@@ -0,0 +1,148 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+
5
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
7
+	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
9
+
10
+	"github.com/openshift/origin/pkg/security/mcs"
11
+	"github.com/openshift/origin/pkg/security/uid"
12
+	"github.com/openshift/origin/pkg/security/uidallocator"
13
+)
14
+
15
+const (
16
+	uidRangeAnnotation = "openshift.io/sa.scc.uid-range"
17
+	mcsAnnotation      = "openshift.io/sa.scc.mcs"
18
+)
19
+
20
+type MCSAllocationFunc func(uid.Block) *mcs.Label
21
+
22
+// DefaultMCSAllocation returns a label from the MCS range that matches the offset
23
+// within the overall range. blockSize must be a positive integer representing the
24
+// number of labels to jump past in the category space (if 1, range == label, if 2
25
+// each range will have two labels).
26
+func DefaultMCSAllocation(from *uid.Range, to *mcs.Range, blockSize int) MCSAllocationFunc {
27
+	return func(block uid.Block) *mcs.Label {
28
+		ok, offset := from.Offset(block)
29
+		if !ok {
30
+			return nil
31
+		}
32
+		if blockSize > 0 {
33
+			offset = offset * uint32(blockSize)
34
+		}
35
+		label, _ := to.LabelAt(uint64(offset))
36
+		return label
37
+	}
38
+}
39
+
40
+type Allocation struct {
41
+	uid    uidallocator.Interface
42
+	mcs    MCSAllocationFunc
43
+	client kclient.NamespaceInterface
44
+}
45
+
46
+// retryCount is the number of times to retry on a conflict when updating a namespace
47
+const retryCount = 2
48
+
49
+// Next processes a changed namespace and tries to allocate a uid range for it.  If it is
50
+// successful, an mcs label corresponding to the relative position of the range is also
51
+// set.
52
+func (c *Allocation) Next(ns *kapi.Namespace) error {
53
+	tx := &tx{}
54
+	defer tx.Rollback()
55
+
56
+	if _, ok := ns.Annotations[uidRangeAnnotation]; ok {
57
+		return nil
58
+	}
59
+
60
+	if ns.Annotations == nil {
61
+		ns.Annotations = make(map[string]string)
62
+	}
63
+
64
+	// do uid allocation
65
+	block, err := c.uid.AllocateNext()
66
+	if err != nil {
67
+		return err
68
+	}
69
+	tx.Add(func() error { return c.uid.Release(block) })
70
+	ns.Annotations[uidRangeAnnotation] = block.String()
71
+	if _, ok := ns.Annotations[mcsAnnotation]; !ok {
72
+		if label := c.mcs(block); label != nil {
73
+			ns.Annotations[mcsAnnotation] = label.String()
74
+		}
75
+	}
76
+
77
+	// TODO: could use a client.GuaranteedUpdate/Merge function
78
+	for i := 0; i < retryCount; i++ {
79
+		_, err := c.client.Update(ns)
80
+		if err == nil {
81
+			// commit and exit
82
+			tx.Commit()
83
+			return nil
84
+		}
85
+
86
+		if errors.IsNotFound(err) {
87
+			return nil
88
+		}
89
+		if !errors.IsConflict(err) {
90
+			return err
91
+		}
92
+		newNs, err := c.client.Get(ns.Name)
93
+		if errors.IsNotFound(err) {
94
+			return nil
95
+		}
96
+		if err != nil {
97
+			return err
98
+		}
99
+		if changedAndSetAnnotations(ns, newNs) {
100
+			return nil
101
+		}
102
+
103
+		// try again
104
+		if newNs.Annotations == nil {
105
+			newNs.Annotations = make(map[string]string)
106
+		}
107
+		newNs.Annotations[uidRangeAnnotation] = ns.Annotations[uidRangeAnnotation]
108
+		newNs.Annotations[mcsAnnotation] = ns.Annotations[mcsAnnotation]
109
+		ns = newNs
110
+	}
111
+
112
+	return fmt.Errorf("unable to allocate security info on %q after %d retries", ns.Name, retryCount)
113
+}
114
+
115
+func changedAndSetAnnotations(old, ns *kapi.Namespace) bool {
116
+	if value, ok := ns.Annotations[uidRangeAnnotation]; ok && value != old.Annotations[uidRangeAnnotation] {
117
+		return true
118
+	}
119
+	if value, ok := ns.Annotations[mcsAnnotation]; ok && value != old.Annotations[mcsAnnotation] {
120
+		return true
121
+	}
122
+	return false
123
+}
124
+
125
+type tx struct {
126
+	rollback []func() error
127
+}
128
+
129
+func (tx *tx) Add(fn func() error) {
130
+	tx.rollback = append(tx.rollback, fn)
131
+}
132
+
133
+func (tx *tx) HasChanges() bool {
134
+	return len(tx.rollback) > 0
135
+}
136
+
137
+func (tx *tx) Rollback() {
138
+	for _, fn := range tx.rollback {
139
+		if err := fn(); err != nil {
140
+			util.HandleError(fmt.Errorf("unable to undo tx: %v", err))
141
+		}
142
+	}
143
+}
144
+
145
+func (tx *tx) Commit() {
146
+	tx.rollback = nil
147
+}
0 148
new file mode 100644
... ...
@@ -0,0 +1,111 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+	"testing"
6
+
7
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
11
+
12
+	"github.com/openshift/origin/pkg/security/mcs"
13
+	"github.com/openshift/origin/pkg/security/uid"
14
+	"github.com/openshift/origin/pkg/security/uidallocator"
15
+)
16
+
17
+func TestController(t *testing.T) {
18
+	var action testclient.FakeAction
19
+	client := &testclient.Fake{
20
+		ReactFn: func(a testclient.FakeAction) (runtime.Object, error) {
21
+			action = a
22
+			return (*kapi.Namespace)(nil), nil
23
+		},
24
+	}
25
+	uidr, _ := uid.NewRange(10, 20, 2)
26
+	mcsr, _ := mcs.NewRange("s0:", 10, 2)
27
+	uida := uidallocator.NewInMemory(uidr)
28
+	c := Allocation{
29
+		uid:    uida,
30
+		mcs:    DefaultMCSAllocation(uidr, mcsr, 5),
31
+		client: client.Namespaces(),
32
+	}
33
+
34
+	err := c.Next(&kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "test"}})
35
+	if err != nil {
36
+		t.Fatal(err)
37
+	}
38
+
39
+	got := action.Value.(*kapi.Namespace)
40
+	if got.Annotations[uidRangeAnnotation] != "10/2" {
41
+		t.Errorf("unexpected annotation: %#v", got)
42
+	}
43
+	if got.Annotations[mcsAnnotation] != "s0:c1,c0" {
44
+		t.Errorf("unexpected annotation: %#v", got)
45
+	}
46
+	if !uida.Has(uid.Block{Start: 10, End: 11}) {
47
+		t.Errorf("did not allocate uid: %#v", uida)
48
+	}
49
+}
50
+
51
+func TestControllerError(t *testing.T) {
52
+	testCases := map[string]struct {
53
+		err     func() error
54
+		errFn   func(err error) bool
55
+		reactFn testclient.ReactionFunc
56
+		actions int
57
+	}{
58
+		"not found": {
59
+			err:     func() error { return errors.NewNotFound("namespace", "test") },
60
+			errFn:   func(err error) bool { return err == nil },
61
+			actions: 1,
62
+		},
63
+		"unknown": {
64
+			err:     func() error { return fmt.Errorf("unknown") },
65
+			errFn:   func(err error) bool { return err.Error() == "unknown" },
66
+			actions: 1,
67
+		},
68
+		"conflict": {
69
+			actions: 4,
70
+			reactFn: func(a testclient.FakeAction) (runtime.Object, error) {
71
+				if a.Action == "get-namespace" {
72
+					return &kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "test"}}, nil
73
+				}
74
+				return (*kapi.Namespace)(nil), errors.NewConflict("namespace", "test", fmt.Errorf("test conflict"))
75
+			},
76
+			errFn: func(err error) bool {
77
+				return err != nil && strings.Contains(err.Error(), "unable to allocate security info")
78
+			},
79
+		},
80
+	}
81
+
82
+	for s, testCase := range testCases {
83
+		client := &testclient.Fake{ReactFn: testCase.reactFn}
84
+		if client.ReactFn == nil {
85
+			client.ReactFn = func(a testclient.FakeAction) (runtime.Object, error) {
86
+				return (*kapi.Namespace)(nil), testCase.err()
87
+			}
88
+		}
89
+		uidr, _ := uid.NewRange(10, 19, 2)
90
+		mcsr, _ := mcs.NewRange("s0:", 10, 2)
91
+		uida := uidallocator.NewInMemory(uidr)
92
+		c := Allocation{
93
+			uid:    uida,
94
+			mcs:    DefaultMCSAllocation(uidr, mcsr, 5),
95
+			client: client.Namespaces(),
96
+		}
97
+
98
+		err := c.Next(&kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "test"}})
99
+		if !testCase.errFn(err) {
100
+			t.Errorf("%s: unexpected error: %v", s, err)
101
+		}
102
+
103
+		if len(client.Actions) != testCase.actions {
104
+			t.Errorf("%s: expected %d actions: %v", s, testCase.actions, client.Actions)
105
+		}
106
+		if uida.Free() != 5 {
107
+			t.Errorf("%s: should not have allocated uid: %d/%d", s, uida.Free(), uidr.Size())
108
+		}
109
+	}
110
+}
0 111
new file mode 100644
... ...
@@ -0,0 +1,68 @@
0
+package controller
1
+
2
+import (
3
+	"time"
4
+
5
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
6
+	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
11
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
12
+	kutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
13
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
14
+
15
+	"github.com/openshift/origin/pkg/controller"
16
+	"github.com/openshift/origin/pkg/security/uidallocator"
17
+)
18
+
19
+// AllocationFactory can create an Allocation controller.
20
+type AllocationFactory struct {
21
+	UIDAllocator uidallocator.Interface
22
+	MCSAllocator MCSAllocationFunc
23
+	Client       kclient.NamespaceInterface
24
+	// Queue may be a FIFO queue of namespaces. If nil, will be initialized using
25
+	// the client.
26
+	Queue controller.ReQueue
27
+}
28
+
29
+// Create creates a Allocation.
30
+func (f *AllocationFactory) Create() controller.RunnableController {
31
+	if f.Queue == nil {
32
+		lw := &cache.ListWatch{
33
+			ListFunc: func() (runtime.Object, error) {
34
+				return f.Client.List(labels.Everything(), fields.Everything())
35
+			},
36
+			WatchFunc: func(resourceVersion string) (watch.Interface, error) {
37
+				return f.Client.Watch(labels.Everything(), fields.Everything(), resourceVersion)
38
+			},
39
+		}
40
+		q := cache.NewFIFO(cache.MetaNamespaceKeyFunc)
41
+		cache.NewReflector(lw, &kapi.Namespace{}, q, 10*time.Minute).Run()
42
+		f.Queue = q
43
+	}
44
+
45
+	c := &Allocation{
46
+		uid:    f.UIDAllocator,
47
+		mcs:    f.MCSAllocator,
48
+		client: f.Client,
49
+	}
50
+
51
+	return &controller.RetryController{
52
+		Queue: f.Queue,
53
+		RetryManager: controller.NewQueueRetryManager(
54
+			f.Queue,
55
+			cache.MetaNamespaceKeyFunc,
56
+			func(obj interface{}, err error, retries controller.Retry) bool {
57
+				util.HandleError(err)
58
+				return retries.Count < 5
59
+			},
60
+			kutil.NewTokenBucketRateLimiter(1, 10),
61
+		),
62
+		Handle: func(obj interface{}) error {
63
+			r := obj.(*kapi.Namespace)
64
+			return c.Next(r)
65
+		},
66
+	}
67
+}
0 68
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"time"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
11
+
12
+	"github.com/openshift/origin/pkg/security/uid"
13
+	"github.com/openshift/origin/pkg/security/uidallocator"
14
+)
15
+
16
+// Repair is a controller loop that periodically examines all UID allocations
17
+// and logs any errors, and then sets the compacted and accurate list of both
18
+//
19
+// Can be run at infrequent intervals, and is best performed on startup of the master.
20
+// Is level driven and idempotent - all claimed UIDs will be updated into the allocator
21
+// map at the end of a single execution loop if no race is encountered.
22
+//
23
+type Repair struct {
24
+	interval time.Duration
25
+	client   client.NamespaceInterface
26
+	alloc    service.RangeRegistry
27
+	uidRange *uid.Range
28
+}
29
+
30
+// NewRepair creates a controller that periodically ensures that all UIDs labels that are allocated in the cluster
31
+// are claimed.
32
+func NewRepair(interval time.Duration, client client.NamespaceInterface, uidRange *uid.Range, alloc service.RangeRegistry) *Repair {
33
+	return &Repair{
34
+		interval: interval,
35
+		client:   client,
36
+		uidRange: uidRange,
37
+		alloc:    alloc,
38
+	}
39
+}
40
+
41
+// RunUntil starts the controller until the provided ch is closed.
42
+func (c *Repair) RunUntil(ch chan struct{}) {
43
+	util.Until(func() {
44
+		if err := c.RunOnce(); err != nil {
45
+			util.HandleError(err)
46
+		}
47
+	}, c.interval, ch)
48
+}
49
+
50
+// RunOnce verifies the state of allocations and returns an error if an unrecoverable problem occurs.
51
+func (c *Repair) RunOnce() error {
52
+	// TODO: (per smarterclayton) if Get() or List() is a weak consistency read,
53
+	// or if they are executed against different leaders,
54
+	// the ordering guarantee required to ensure no item is allocated twice is violated.
55
+	// List must return a ResourceVersion higher than the etcd index Get,
56
+	// and the release code must not release items that have allocated but not yet been created
57
+	// See #8295
58
+
59
+	latest, err := c.alloc.Get()
60
+	if err != nil {
61
+		return fmt.Errorf("unable to refresh the security allocation UID blocks: %v", err)
62
+	}
63
+
64
+	list, err := c.client.List(labels.Everything(), fields.Everything())
65
+	if err != nil {
66
+		return fmt.Errorf("unable to refresh the security allocation UID blocks: %v", err)
67
+	}
68
+
69
+	uids := uidallocator.NewInMemory(c.uidRange)
70
+
71
+	for _, ns := range list.Items {
72
+		value, ok := ns.Annotations[uidRangeAnnotation]
73
+		if !ok {
74
+			continue
75
+		}
76
+		block, err := uid.ParseBlock(value)
77
+		if err != nil {
78
+			continue
79
+		}
80
+		switch err := uids.Allocate(block); err {
81
+		case nil:
82
+		case uidallocator.ErrFull:
83
+			// TODO: send event
84
+			return fmt.Errorf("the UID range %s is full; you must widen the range in order to allocate more UIDs", c.uidRange)
85
+		default:
86
+			return fmt.Errorf("unable to allocate UID block %s for namespace %s due to an unknown error, exiting: %v", block, ns.Name, err)
87
+		}
88
+	}
89
+
90
+	err = uids.Snapshot(latest)
91
+	if err != nil {
92
+		return fmt.Errorf("unable to persist the updated namespace UID allocations: %v", err)
93
+	}
94
+
95
+	if err := c.alloc.CreateOrUpdate(latest); err != nil {
96
+		return fmt.Errorf("unable to persist the updated namespace UID allocations: %v", err)
97
+	}
98
+	return nil
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,63 @@
0
+package controller
1
+
2
+import (
3
+	"testing"
4
+	"time"
5
+
6
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
9
+
10
+	"github.com/openshift/origin/pkg/security/uid"
11
+)
12
+
13
+type fakeRange struct {
14
+	Err       error
15
+	Range     *kapi.RangeAllocation
16
+	Updated   *kapi.RangeAllocation
17
+	UpdateErr error
18
+}
19
+
20
+func (r *fakeRange) Get() (*kapi.RangeAllocation, error) {
21
+	return r.Range, r.Err
22
+}
23
+
24
+func (r *fakeRange) CreateOrUpdate(update *kapi.RangeAllocation) error {
25
+	r.Updated = update
26
+	return r.UpdateErr
27
+}
28
+
29
+func TestRepair(t *testing.T) {
30
+	var action testclient.FakeAction
31
+	client := &testclient.Fake{
32
+		ReactFn: func(a testclient.FakeAction) (runtime.Object, error) {
33
+			action = a
34
+			list := &kapi.NamespaceList{
35
+				Items: []kapi.Namespace{
36
+					{ObjectMeta: kapi.ObjectMeta{Name: "default"}},
37
+				},
38
+			}
39
+			return list, nil
40
+		},
41
+	}
42
+	alloc := &fakeRange{
43
+		Range: &kapi.RangeAllocation{},
44
+	}
45
+
46
+	uidr, _ := uid.NewRange(10, 20, 2)
47
+	repair := NewRepair(0*time.Second, client.Namespaces(), uidr, alloc)
48
+
49
+	err := repair.RunOnce()
50
+	if err != nil {
51
+		t.Fatal(err)
52
+	}
53
+	if alloc.Updated == nil {
54
+		t.Fatalf("did not store range: %#v", alloc)
55
+	}
56
+	if alloc.Updated.Range != "10-20/2" {
57
+		t.Errorf("didn't store range properly: %#v", alloc.Updated)
58
+	}
59
+	if len(alloc.Updated.Data) != 0 {
60
+		t.Errorf("data wasn't empty: %#v", alloc.Updated)
61
+	}
62
+}
... ...
@@ -41,6 +41,11 @@ func New(r *mcs.Range, factory allocator.AllocatorFactory) *Allocator {
41 41
 	}
42 42
 }
43 43
 
44
+// NewInMemory creates an in-memory Allocator
45
+func NewInMemory(r *mcs.Range) *Allocator {
46
+	return New(r, allocator.NewContiguousAllocationInterface)
47
+}
48
+
44 49
 // Free returns the count of port left in the range.
45 50
 func (r *Allocator) Free() int {
46 51
 	return r.alloc.Free()
... ...
@@ -2,6 +2,7 @@ package uid
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"strings"
5 6
 )
6 7
 
7 8
 type Block struct {
... ...
@@ -9,7 +10,35 @@ type Block struct {
9 9
 	End   uint32
10 10
 }
11 11
 
12
+func ParseBlock(in string) (Block, error) {
13
+	if strings.Contains(in, "/") {
14
+		var start, size uint32
15
+		n, err := fmt.Sscanf(in, "%d/%d", &start, &size)
16
+		if err != nil {
17
+			return Block{}, err
18
+		}
19
+		if n != 2 {
20
+			return Block{}, fmt.Errorf("block not in the format \"<start>/<size>\"")
21
+		}
22
+		return Block{Start: start, End: start + size - 1}, nil
23
+	}
24
+
25
+	var start, end uint32
26
+	n, err := fmt.Sscanf(in, "%d-%d", &start, &end)
27
+	if err != nil {
28
+		return Block{}, err
29
+	}
30
+	if n != 2 {
31
+		return Block{}, fmt.Errorf("block not in the format \"<start>-<end>\"")
32
+	}
33
+	return Block{Start: start, End: end}, nil
34
+}
35
+
12 36
 func (b Block) String() string {
37
+	return fmt.Sprintf("%d/%d", b.Start, b.Size())
38
+}
39
+
40
+func (b Block) RangeString() string {
13 41
 	return fmt.Sprintf("%d-%d", b.Start, b.End)
14 42
 }
15 43
 
... ...
@@ -55,7 +84,7 @@ func (r *Range) Size() uint32 {
55 55
 }
56 56
 
57 57
 func (r *Range) String() string {
58
-	return fmt.Sprintf("%s/%d", r.block, r.size)
58
+	return fmt.Sprintf("%s/%d", r.block.RangeString(), r.size)
59 59
 }
60 60
 
61 61
 func (r *Range) BlockAt(offset uint32) (Block, bool) {
... ...
@@ -72,6 +72,20 @@ func TestParseRange(t *testing.T) {
72 72
 	}
73 73
 }
74 74
 
75
+func TestBlock(t *testing.T) {
76
+	b := Block{Start: 100, End: 109}
77
+	if b.String() != "100/10" {
78
+		t.Errorf("unexpected block string: %s", b.String())
79
+	}
80
+	b, err := ParseBlock("100-109")
81
+	if err != nil {
82
+		t.Fatal(err)
83
+	}
84
+	if b.String() != "100/10" {
85
+		t.Errorf("unexpected block string: %s", b.String())
86
+	}
87
+}
88
+
75 89
 func TestOffset(t *testing.T) {
76 90
 	testCases := map[string]struct {
77 91
 		r         Range
... ...
@@ -41,6 +41,11 @@ func New(r *uid.Range, factory allocator.AllocatorFactory) *Allocator {
41 41
 	}
42 42
 }
43 43
 
44
+// NewInMemory creates an in-memory Allocator
45
+func NewInMemory(r *uid.Range) *Allocator {
46
+	return New(r, allocator.NewContiguousAllocationInterface)
47
+}
48
+
44 49
 // Free returns the count of port left in the range.
45 50
 func (r *Allocator) Free() int {
46 51
 	return r.alloc.Free()