Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource allocator interface changes #36

Merged
merged 6 commits into from
Apr 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ clean-demo:
unit-test: build
CONTIV_HOST_GOBIN=$(HOST_GOBIN) CONTIV_HOST_GOROOT=$(HOST_GOROOT) ./scripts/unittests -vagrant

# setting CONTIV_SOE=1 while calling 'make system-test' will stop the test
# on first failure and leave setup in that state. This can be useful for debugging
# as part of development.
system-test: build
go test -v -run "sanity" github.com/contiv/netplugin/systemtests/singlehost
go test --timeout 20m -v -run "sanity" github.com/contiv/netplugin/systemtests/twohosts

# setting CONTIV_SOE=1 while calling 'make regress-test' will stop the test
# on first failure and leave setup in that state. This can be useful for debugging
# as part of development.
regress-test: build
go test -v -run "regress" github.com/contiv/netplugin/systemtests/singlehost
go test --timeout 60m -v -run "regress" github.com/contiv/netplugin/systemtests/twohosts
31 changes: 25 additions & 6 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ type Address struct {
addr string
}

type State interface {
Write() error
Read(id string) error
Clear() error
}

type Config struct {
// Config object parsed from a json styled config
V interface{}
Expand Down Expand Up @@ -103,5 +97,30 @@ type StateDriver interface {
marshal func(interface{}) ([]byte, error)) error
ReadState(key string, value State,
unmarshal func([]byte, interface{}) error) error
ReadAllState(baseKey string, stateType State,
unmarshal func([]byte, interface{}) error) ([]State, error)
ClearState(key string) error
}

type Resource interface {
// Resource defines a allocatable unit. A resource is uniquely identified
// by 'Id'. A resource description identifies the nature of the resource.
State
Init(rsrcCfg interface{}) error
Deinit()
Description() string
Allocate() (interface{}, error)
Deallocate(interface{}) error
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource Interface:

  • State should be moved out of resource interface (as alluded in the code). I'd suggest we specify the State Driver and State Config during Init() and then just call allocate/de-allocate? State Config can define the path/hierarchy given to a specific resource to manage its own hierarchy there in. If State Driver is passed during Init then we'd not need SetStateDriver() call.
  • A Get() or Read() routine is perhaps okay, but would not take State parameter I suppose.
  • SetId() seem to suggest that I can allocate a resource first and then set its id, so then how do I key the resource in between? Should we just say that keep the Id must be specified during NewResource() type of function?
  • ReadAll is a need only because we organize this as resource allocator
  • Resources Dependency: how do we capture that to allocate one resource we need depend on another one. For example, IP allocation may depend on IP Subnet allocation.
  • Resource being global or local: I think we shouldn't explicitly call it out to be global, because allocator package can be used by local/host entity as well. This would mean that agent on the host can perhaps use the resource allocation for local elements that are not cluster-wide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State should be moved out of resource interface (as alluded in the code).

I am not sure if I understood. I model Resource interface as a (config) State along with associated logic for allocation and de-allocation. So in that sense it seem reasonable to reuse the same State interface instead of defining Read/Write methods of this own. Does the code seem to imply otherwise?

State Config can define the path/hierarchy given to a specific resource to manage its own hierarchy there in. If State Driver is passed during Init then we'd not need SetStateDriver() call.

IMO moving knowledge about hierachy/path outside the State exposes the low level details of the state organization to the caller. This information is better kept inside the State. However, there are some common parts to all the State like Id and StateDriver, which I think can be moved to the State interface itself (as I also point to in my comment). I don't want to have Init() method for State since it seems similar to having a constructor for entire State which will differ between different implementations.

SetId() seem to suggest that I can allocate a resource first and then set its id, so then how do I key the resource in between?

Since the resource get's defined through ResourceAllocator so this is avoided. However, I realize that same effect might appear when I move this method to the State interface. I think to make it more clear we can call it InitId(), InitStateDriver() instead.

ReadAll is a need only because we organize this as resource allocator

Yes, that is one case. I think ReadAll is needed for all State(s) (even though we need to figure how to address it for other data stores). But use ReadAll kind of function else where as well. Example, we depend on ReadAll() for reading existing configurations to compute deltas etc. We can also use it for dumping (in pretty format) all system-state for debugging etc.

Resources Dependency: how do we capture that to allocate one resource we need depend on another one. For example, IP allocation may depend on IP Subnet allocation.

The way I have defined, each resource is defined independently and the information passed during it's definition shall be enough to initialize it. So, in this case the IP resource will require a Subnet to be provided in it's definition, which in turn will require that Subnet to have been allocated.

Resource being global or local: I think we shouldn't explicitly call it out to be global,

Yes, the ResourceAllocator shall be usable for both local and global resource. The difference really comes from which implementation is called where. Example, a local ResourceAllocator need not implement state synchronization algorithms.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State should be moved out of resource interface (as alluded in the code).

I am not sure if I understood. I model Resource interface as a (config) State along with associated logic for allocation and de-allocation. So in that sense it seem reasonable to reuse the same State interface instead of defining Read/Write methods of this own. Does the code seem to imply otherwise?

Maintaining state is a natural effect of resource allocation/deallocation/init, etc. Then resource is merely an object that can be allocated, deallocated, updated, etc. The associated logic, its state state need not be made as part of the interface. However it needs to know the stateDriver therefore it can be passed during Init()

IMO moving knowledge about hierachy/path outside the State exposes the low level details of the state organization to the caller. This information is better kept inside the State. However, there are some common parts to all the State like Id and StateDriver, which I think can be moved to the State interface itself (as I also point to in my comment). I don't want to have Init() method for State since it seems similar to having a constructor for entire State which will differ between different implementations.

There is a logical hierarchy of various objects, now whether state interprets and maintains the same hierarchy can be left out of the logical hierarchy. Right now the key hierarchy is split among various files and no central place to know the central hierarchy. I 'd not be very concerned if we create the logical hierarchy using dir/file syntax or some other notation.

The way I have defined, each resource is defined independently and the information passed during it's definition shall be enough to initialize it. So, in this case the IP resource will require a Subnet to be provided in it's definition, which in turn will require that Subnet to have been allocated.

Sure. I was hoping that resource allocation can happen in a nested manner i.e. one resource allocation can trigger another resource to be allocated. For example if an load-balancer resource is allocated, it may automatically suggest allocating an IP address from the pool.

Yes, the ResourceAllocator shall be usable for both local and global resource. The difference really comes from which implementation is called where. Example, a local ResourceAllocator need not implement state synchronization algorithms.

ok; let me ask the question differently. If I use vlanResource on the host for local vlan pool management, do I need a different implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The associated logic, its state state need not be made as part of the interface. However it needs to know the stateDriver therefore it can be passed during Init()

I would like to reuse the State as a underlying interface for whatever can hold state. This has certain benefits like it reuses a lot of interfaces without having to define new ones; it provides a uniform way to access the information in the system; it promotes a uniform (State based) design for the major constructs in our system i.e we think about our constructs as State and associated behaviors.

I 'd not be very concerned if we create the logical hierarchy using dir/file syntax or some other notation.

Are you concerned about overlapping paths between different states (as they are defined in different files) in etcd? I guess as of now that hasn't been an issue since the states we have defined have been for different functions like driver, resources etc and that might be the case going forward as well. In my view, the paths are not first order citizens (since we hide them in State). We should make sure that higher level functions that build on these states don't make assumptions on this hierarchy.

For example if an load-balancer resource is allocated, it may automatically suggest allocating an IP address from the pool.

hmm, may be the definition of resource needs to be clarified. What does a load-balancer resource implies in your mind, is it just like an endpoint with IP address derived from a subnet pool allocated for it's network previously (at time of network definition) Or does it also involves auto defining a network and then grabbing an IP in that network? In either case there is a stage of defining the network and subnet allocation can occur at that point, right?

If I use vlanResource on the host for local vlan pool management, do I need a different implementation?

I think it depends on where the ResourceAllocator logic can be run to manage these vlans. If the local vlans are an independent set that can be managed locally then a different implementation of ResourceAllocator (running at netplugin level) might be needed. But if the vlanResource needs knowledge of centrally allocated vlans (as is the case for us today), then this ResourceAllocator will be same as the one for the cluster (i.e. it runs at netmaster level). An example of local/host-level resources that will need a different ResourceAllocator implementation (run at netplugin level) I can think of are network queues/buffers. Vxlan-only networking (i.e. no mixed mode at all) can potentially make use of a host-only ResourceAllocator as well for just allocating local-vlans per host but in that case I don't see benefit of having two implementations since single implementation can allocate same vlans on all hosts without having to worry about overlaps.

type ResourceManager interface {
// A resource manager provides mechanism to manage (define/undefine,
// allocate/deallocate) resources. Example, it may provide management in
// logically centralized manner in a distributed system
Init() error
Deinit()
DefineResource(id, desc string, rsrcCfg interface{}) error
UndefineResource(id, desc string) error
AllocateResourceVal(id, desc string) (interface{}, error)
DeallocateResourceVal(id, desc string, value interface{}) error
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Sorry very basic question - what is the difference between Resource and ResourceAllocator? With another indirection, for resource allocation, are we making it unnecessarily complicated?
  • Why is ResourceAllocator not taking Resource as input, rather an interface{} which gets converted to a Resource.
  • This will eliminate the need for implementing Resource Allocator for etcd, counsul, zk, etc. separately. i.e. following logic (in resource/etc/resourceallocator.go) is a repeat IMHO for these various allocators

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference between Resource and ResourceAllocator? With another indirection, for resource allocation, are we making it unnecessarily complicated?

A ResourceAllocator manages resources by providing necessary synchronization, provision for cross-resource checks etc. The synchronization mechanisms will differ between the data store like etcd, consul, zookeeper etc and hence require a different implementation of ResourceAllocator.
A Resource defines the nature and allocation/deallocation logic of the underlying resource without worrying about the management.
A ResourceAllocator would be written once and rarely modified but Resources would be defined more often. Hence, having this abstraction actually simplifies things for the developers who define new resources.

Why is ResourceAllocator not taking Resource as input, rather an interface{} which gets converted to a Resource.

I passed an interface{} instead of resource because the interface just defines the configuration needed to be passed than the entire Resource. This allows for the leeway for the Resource implementer to accept configuration in any structure and initialize the Resource out of it. Defining the argument to be Resource instead might give the implication that the caller provides details like state-driver etc as well.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ResourceAllocator manages resources by providing necessary synchronization, provision for cross-resource checks etc. The synchronization mechanisms will differ between the data store like etcd, consul, zookeeper etc and hence require a different implementation of ResourceAllocator.
A Resource defines the nature and allocation/deallocation logic of the underlying resource without worrying about the management.
A ResourceAllocator would be written once and rarely modified but Resources would be defined more often. Hence, having this abstraction actually simplifies things for the developers who define new resources.

If ResourceAllocator is mainly managing the synchronization of state, then perhaps it be called StateResourceManager, however your comment about cross resource checks is interesting. Can you give an example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ResourceAllocator is mainly managing the synchronization of state, then perhaps it be called StateResourceManager,

Sure that makes sense, I shall rename it to ResourceManager.

your comment about cross resource checks is interesting. Can you give an example?

Cross checks might involve, checking potential overlaps between two resource. Example, autoVlan resource with range 100-200 can't coexist with autoVlan resource with range 150-250 due to an overlap.

Cross checks might also involve auto deriving some information during a resource definition based on existing resources. Eg. if autoVlan resource is defined for 1-200, then defining a autoVxlan resource 3000-3100 can potentially automatically pick local-vlans from 201-301.

Alternatively, such checks can be done at configuration parsing level as well, as long as they can be done in a logically-central manner.

16 changes: 16 additions & 0 deletions core/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package core

type State interface {
Read(id string) error
ReadAll() ([]State, error)
Write() error
Clear() error
}

// CommonState defines the fields common to all core.State implementations.
// This struct shall be embedded as anonymous field in all structs that
// implement core.State
type CommonState struct {
StateDriver StateDriver `json:"-"`
Id string `json:"id"`
}
45 changes: 44 additions & 1 deletion drivers/etcdstatedriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ package drivers

import (
"fmt"
"github.com/contiv/go-etcd/etcd"
"reflect"

"github.com/contiv/go-etcd/etcd"
"github.com/contiv/netplugin/core"
)

Expand Down Expand Up @@ -103,6 +104,48 @@ func (d *EtcdStateDriver) ReadState(key string, value core.State,
return nil
}

// XXX: move this to some common file
func ReadAllStateCommon(d core.StateDriver, baseKey string, sType core.State,
unmarshal func([]byte, interface{}) error) ([]core.State, error) {
stateType := reflect.TypeOf(sType)
sliceType := reflect.SliceOf(stateType)
values := reflect.MakeSlice(sliceType, 0, 1)

byteValues, err := d.ReadAll(baseKey)
if err != nil {
return nil, err
}
for _, byteValue := range byteValues {
value := reflect.New(stateType)
err = unmarshal(byteValue, value.Interface())
if err != nil {
return nil, err
}
values = reflect.Append(values, value.Elem())
}

stateValues := []core.State{}
for i := 0; i < values.Len(); i++ {
// sanity checks
if !values.Index(i).Elem().FieldByName("CommonState").IsValid() {
panic(fmt.Sprintf("The state structure %v is missing core.CommonState",
stateType))
return nil, &core.Error{Desc: fmt.Sprintf("The state structure %v is missing core.CommonState",
stateType)}
}
//the following works as every core.State is expected to embed core.CommonState struct
values.Index(i).Elem().FieldByName("CommonState").FieldByName("StateDriver").Set(reflect.ValueOf(d))
stateValue := values.Index(i).Interface().(core.State)
stateValues = append(stateValues, stateValue)
}
return stateValues, nil
}

func (d *EtcdStateDriver) ReadAllState(baseKey string, sType core.State,
unmarshal func([]byte, interface{}) error) ([]core.State, error) {
return ReadAllStateCommon(d, baseKey, sType, unmarshal)
}

func (d *EtcdStateDriver) WriteState(key string, value core.State,
marshal func(interface{}) ([]byte, error)) error {
encodedState, err := marshal(value)
Expand Down
24 changes: 4 additions & 20 deletions drivers/etcdstatedriver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,6 @@ import (
"github.com/contiv/netplugin/core"
)

// setup a etcd cluster, run tests and then cleanup the cluster
// XXX: enabled once I upgrade to golang 1.4
//func TestMain(m *testing.M) {
//
// // start etcd
// proc, err := os.StartProcess("etcd", []string{}, nil)
// if err != nil {
// log.Printf("failed to start etcd. Error: %s", err)
// os.Exit(-1)
// }
//
// //run the tests
// exitC := m.Run()
//
// // stop etcd
// proc.Kill()
//
// os.Exit(exitC)
//}

func setupDriver(t *testing.T) *EtcdStateDriver {
etcdConfig := &EtcdStateDriverConfig{}
etcdConfig.Etcd.Machines = []string{"http://127.0.0.1:4001"}
Expand Down Expand Up @@ -125,6 +105,10 @@ func (s *testState) Read(id string) error {
return &core.Error{Desc: "Should not be called!!"}
}

func (s *testState) ReadAll() ([]core.State, error) {
return nil, &core.Error{Desc: "Should not be called!!"}
}

func (s *testState) Clear() error {
return &core.Error{Desc: "Should not be called!!"}
}
Expand Down
103 changes: 103 additions & 0 deletions drivers/fakestatedriver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package drivers

import (
"log"
"strings"

"github.com/contiv/netplugin/core"
)

// The FakeStateDriver implements core.StateDriver interface for use with
// unit-tests

type ValueData struct {
value []byte
}

type FakeStateDriver struct {
TestState map[string]ValueData
}

func (d *FakeStateDriver) Init(config *core.Config) error {
d.TestState = make(map[string]ValueData)

return nil
}

func (d *FakeStateDriver) Deinit() {
d.TestState = nil
}

func (d *FakeStateDriver) Write(key string, value []byte) error {
val := ValueData{value: value}
d.TestState[key] = val

return nil
}

func (d *FakeStateDriver) Read(key string) ([]byte, error) {
if val, ok := d.TestState[key]; ok {
return val.value, nil
}

return []byte{}, &core.Error{Desc: "Key not found!"}
}

func (d *FakeStateDriver) ReadAll(baseKey string) ([][]byte, error) {
values := [][]byte{}

for key, val := range d.TestState {
if strings.Contains(key, baseKey) {
values = append(values, val.value)
}
}
return values, nil
}

func (d *FakeStateDriver) ClearState(key string) error {
if _, ok := d.TestState[key]; ok {
delete(d.TestState, key)
}
return nil
}

func (d *FakeStateDriver) ReadState(key string, value core.State,
unmarshal func([]byte, interface{}) error) error {
encodedState, err := d.Read(key)
if err != nil {
return err
}

err = unmarshal(encodedState, value)
if err != nil {
return err
}

return nil
}

func (d *FakeStateDriver) ReadAllState(baseKey string, sType core.State,
unmarshal func([]byte, interface{}) error) ([]core.State, error) {
return ReadAllStateCommon(d, baseKey, sType, unmarshal)
}

func (d *FakeStateDriver) WriteState(key string, value core.State,
marshal func(interface{}) ([]byte, error)) error {
encodedState, err := marshal(value)
if err != nil {
return err
}

err = d.Write(key, encodedState)
if err != nil {
return err
}

return nil
}

func (d *FakeStateDriver) DumpState() {
for key, _ := range d.TestState {
log.Printf("key: %q\n", key)
}
}
38 changes: 22 additions & 16 deletions drivers/ovsdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ func vxlanIfName(netId, vtepIp string) string {
}

func (d *OvsDriver) createVtep(epCfg *OvsCfgEndpointState) error {
cfgNw := OvsCfgNetworkState{StateDriver: d.stateDriver}
cfgNw := OvsCfgNetworkState{}
cfgNw.StateDriver = d.stateDriver
err := cfgNw.Read(epCfg.NetId)
if err != nil {
return err
Expand All @@ -352,7 +353,8 @@ func (d *OvsDriver) createVtep(epCfg *OvsCfgEndpointState) error {

func (d *OvsDriver) deleteVtep(epOper *OvsOperEndpointState) error {

cfgNw := OvsCfgNetworkState{StateDriver: d.stateDriver}
cfgNw := OvsCfgNetworkState{}
cfgNw.StateDriver = d.stateDriver
err := cfgNw.Read(epOper.NetId)
if err != nil {
return err
Expand Down Expand Up @@ -423,7 +425,8 @@ func (d *OvsDriver) Deinit() {
}

func (d *OvsDriver) CreateNetwork(id string) error {
cfgNw := OvsCfgNetworkState{StateDriver: d.stateDriver}
cfgNw := OvsCfgNetworkState{}
cfgNw.StateDriver = d.stateDriver
err := cfgNw.Read(id)
if err != nil {
log.Printf("Failed to read net %s \n", cfgNw.Id)
Expand All @@ -450,7 +453,8 @@ func (d *OvsDriver) CreateEndpoint(id string) error {
intfName := portName
intfType := "internal"

epCfg := OvsCfgEndpointState{StateDriver: d.stateDriver}
epCfg := OvsCfgEndpointState{}
epCfg.StateDriver = d.stateDriver
err = epCfg.Read(id)
if err != nil {
return err
Expand All @@ -476,7 +480,8 @@ func (d *OvsDriver) CreateEndpoint(id string) error {
intfType = ""
}

cfgNw := OvsCfgNetworkState{StateDriver: d.stateDriver}
cfgNw := OvsCfgNetworkState{}
cfgNw.StateDriver = d.stateDriver
err = cfgNw.Read(epCfg.NetId)
if err != nil {
return err
Expand All @@ -495,16 +500,16 @@ func (d *OvsDriver) CreateEndpoint(id string) error {
}()

operEp := OvsOperEndpointState{
StateDriver: d.stateDriver,
Id: id,
PortName: portName,
NetId: epCfg.NetId,
AttachUUID: epCfg.AttachUUID,
ContName: epCfg.ContName,
IpAddress: epCfg.IpAddress,
IntfName: intfName,
HomingHost: epCfg.HomingHost,
VtepIp: epCfg.VtepIp}
PortName: portName,
NetId: epCfg.NetId,
AttachUUID: epCfg.AttachUUID,
ContName: epCfg.ContName,
IpAddress: epCfg.IpAddress,
IntfName: intfName,
HomingHost: epCfg.HomingHost,
VtepIp: epCfg.VtepIp}
operEp.StateDriver = d.stateDriver
operEp.Id = id
err = operEp.Write()
if err != nil {
return err
Expand All @@ -520,7 +525,8 @@ func (d *OvsDriver) CreateEndpoint(id string) error {

func (d *OvsDriver) DeleteEndpoint(id string) (err error) {

epOper := OvsOperEndpointState{StateDriver: d.stateDriver}
epOper := OvsOperEndpointState{}
epOper.StateDriver = d.stateDriver
err = epOper.Read(id)
if err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions drivers/ovsdriver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ func (d *testOvsStateDriver) ReadState(key string, value core.State,
return &core.Error{Desc: fmt.Sprintf("unknown key! %s", key)}
}

func (d *testOvsStateDriver) ReadAllState(key string, value core.State,
unmarshal func([]byte, interface{}) error) ([]core.State, error) {
return nil, &core.Error{Desc: "shouldn't be called!"}
}

func (d *testOvsStateDriver) WriteState(key string, value core.State,
marshal func(interface{}) ([]byte, error)) error {
return nil
Expand Down
Loading