blob: f30a9a8f7e3007627395e733cd51dc24e1cf5116 [file] [log] [blame]
// Copyright 2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package gomaasapi
import (
"fmt"
"net/http"
"github.com/juju/errors"
"github.com/juju/schema"
"github.com/juju/version"
)
// Can't use interface as a type, so add an underscore. Yay.
type interface_ struct {
controller *controller
resourceURI string
id int
name string
type_ string
enabled bool
tags []string
vlan *vlan
links []*link
macAddress string
effectiveMTU int
parents []string
children []string
}
func (i *interface_) updateFrom(other *interface_) {
i.resourceURI = other.resourceURI
i.id = other.id
i.name = other.name
i.type_ = other.type_
i.enabled = other.enabled
i.tags = other.tags
i.vlan = other.vlan
i.links = other.links
i.macAddress = other.macAddress
i.effectiveMTU = other.effectiveMTU
i.parents = other.parents
i.children = other.children
}
// ID implements Interface.
func (i *interface_) ID() int {
return i.id
}
// Name implements Interface.
func (i *interface_) Name() string {
return i.name
}
// Parents implements Interface.
func (i *interface_) Parents() []string {
return i.parents
}
// Children implements Interface.
func (i *interface_) Children() []string {
return i.children
}
// Type implements Interface.
func (i *interface_) Type() string {
return i.type_
}
// Enabled implements Interface.
func (i *interface_) Enabled() bool {
return i.enabled
}
// Tags implements Interface.
func (i *interface_) Tags() []string {
return i.tags
}
// VLAN implements Interface.
func (i *interface_) VLAN() VLAN {
if i.vlan == nil {
return nil
}
return i.vlan
}
// Links implements Interface.
func (i *interface_) Links() []Link {
result := make([]Link, len(i.links))
for i, link := range i.links {
result[i] = link
}
return result
}
// MACAddress implements Interface.
func (i *interface_) MACAddress() string {
return i.macAddress
}
// EffectiveMTU implements Interface.
func (i *interface_) EffectiveMTU() int {
return i.effectiveMTU
}
// UpdateInterfaceArgs is an argument struct for calling Interface.Update.
type UpdateInterfaceArgs struct {
Name string
MACAddress string
VLAN VLAN
}
func (a *UpdateInterfaceArgs) vlanID() int {
if a.VLAN == nil {
return 0
}
return a.VLAN.ID()
}
// Update implements Interface.
func (i *interface_) Update(args UpdateInterfaceArgs) error {
var empty UpdateInterfaceArgs
if args == empty {
return nil
}
params := NewURLParams()
params.MaybeAdd("name", args.Name)
params.MaybeAdd("mac_address", args.MACAddress)
params.MaybeAddInt("vlan", args.vlanID())
source, err := i.controller.put(i.resourceURI, params.Values)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
switch svrErr.StatusCode {
case http.StatusNotFound:
return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
case http.StatusForbidden:
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
}
}
return NewUnexpectedError(err)
}
response, err := readInterface(i.controller.apiVersion, source)
if err != nil {
return errors.Trace(err)
}
i.updateFrom(response)
return nil
}
// Delete implements Interface.
func (i *interface_) Delete() error {
err := i.controller.delete(i.resourceURI)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
switch svrErr.StatusCode {
case http.StatusNotFound:
return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
case http.StatusForbidden:
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
}
}
return NewUnexpectedError(err)
}
return nil
}
// InterfaceLinkMode is the type of the various link mode constants used for
// LinkSubnetArgs.
type InterfaceLinkMode string
const (
// LinkModeDHCP - Bring the interface up with DHCP on the given subnet. Only
// one subnet can be set to DHCP. If the subnet is managed this interface
// will pull from the dynamic IP range.
LinkModeDHCP InterfaceLinkMode = "DHCP"
// LinkModeStatic - Bring the interface up with a STATIC IP address on the
// given subnet. Any number of STATIC links can exist on an interface.
LinkModeStatic InterfaceLinkMode = "STATIC"
// LinkModeLinkUp - Bring the interface up only on the given subnet. No IP
// address will be assigned to this interface. The interface cannot have any
// current DHCP or STATIC links.
LinkModeLinkUp InterfaceLinkMode = "LINK_UP"
)
// LinkSubnetArgs is an argument struct for passing parameters to
// the Interface.LinkSubnet method.
type LinkSubnetArgs struct {
// Mode is used to describe how the address is provided for the Link.
// Required field.
Mode InterfaceLinkMode
// Subnet is the subnet to link to. Required field.
Subnet Subnet
// IPAddress is only valid when the Mode is set to LinkModeStatic. If
// not specified with a Mode of LinkModeStatic, an IP address from the
// subnet will be auto selected.
IPAddress string
// DefaultGateway will set the gateway IP address for the Subnet as the
// default gateway for the machine or device the interface belongs to.
// Option can only be used with mode LinkModeStatic.
DefaultGateway bool
}
// Validate ensures that the Mode and Subnet are set, and that the other options
// are consistent with the Mode.
func (a *LinkSubnetArgs) Validate() error {
switch a.Mode {
case LinkModeDHCP, LinkModeLinkUp, LinkModeStatic:
case "":
return errors.NotValidf("missing Mode")
default:
return errors.NotValidf("unknown Mode value (%q)", a.Mode)
}
if a.Subnet == nil {
return errors.NotValidf("missing Subnet")
}
if a.IPAddress != "" && a.Mode != LinkModeStatic {
return errors.NotValidf("setting IP Address when Mode is not LinkModeStatic")
}
if a.DefaultGateway && a.Mode != LinkModeStatic {
return errors.NotValidf("specifying DefaultGateway for Mode %q", a.Mode)
}
return nil
}
// LinkSubnet implements Interface.
func (i *interface_) LinkSubnet(args LinkSubnetArgs) error {
if err := args.Validate(); err != nil {
return errors.Trace(err)
}
params := NewURLParams()
params.Values.Add("mode", string(args.Mode))
params.Values.Add("subnet", fmt.Sprint(args.Subnet.ID()))
params.MaybeAdd("ip_address", args.IPAddress)
params.MaybeAddBool("default_gateway", args.DefaultGateway)
source, err := i.controller.post(i.resourceURI, "link_subnet", params.Values)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
switch svrErr.StatusCode {
case http.StatusNotFound, http.StatusBadRequest:
return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
case http.StatusForbidden:
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
case http.StatusServiceUnavailable:
return errors.Wrap(err, NewCannotCompleteError(svrErr.BodyMessage))
}
}
return NewUnexpectedError(err)
}
response, err := readInterface(i.controller.apiVersion, source)
if err != nil {
return errors.Trace(err)
}
i.updateFrom(response)
return nil
}
func (i *interface_) linkForSubnet(subnet Subnet) *link {
for _, link := range i.links {
if s := link.Subnet(); s != nil && s.ID() == subnet.ID() {
return link
}
}
return nil
}
// LinkSubnet implements Interface.
func (i *interface_) UnlinkSubnet(subnet Subnet) error {
if subnet == nil {
return errors.NotValidf("missing Subnet")
}
link := i.linkForSubnet(subnet)
if link == nil {
return errors.NotValidf("unlinked Subnet")
}
params := NewURLParams()
params.Values.Add("id", fmt.Sprint(link.ID()))
source, err := i.controller.post(i.resourceURI, "unlink_subnet", params.Values)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
switch svrErr.StatusCode {
case http.StatusNotFound, http.StatusBadRequest:
return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
case http.StatusForbidden:
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
}
}
return NewUnexpectedError(err)
}
response, err := readInterface(i.controller.apiVersion, source)
if err != nil {
return errors.Trace(err)
}
i.updateFrom(response)
return nil
}
func readInterface(controllerVersion version.Number, source interface{}) (*interface_, error) {
readFunc, err := getInterfaceDeserializationFunc(controllerVersion)
if err != nil {
return nil, errors.Trace(err)
}
checker := schema.StringMap(schema.Any())
coerced, err := checker.Coerce(source, nil)
if err != nil {
return nil, WrapWithDeserializationError(err, "interface base schema check failed")
}
valid := coerced.(map[string]interface{})
return readFunc(valid)
}
func readInterfaces(controllerVersion version.Number, source interface{}) ([]*interface_, error) {
readFunc, err := getInterfaceDeserializationFunc(controllerVersion)
if err != nil {
return nil, errors.Trace(err)
}
checker := schema.List(schema.StringMap(schema.Any()))
coerced, err := checker.Coerce(source, nil)
if err != nil {
return nil, WrapWithDeserializationError(err, "interface base schema check failed")
}
valid := coerced.([]interface{})
return readInterfaceList(valid, readFunc)
}
func getInterfaceDeserializationFunc(controllerVersion version.Number) (interfaceDeserializationFunc, error) {
var deserialisationVersion version.Number
for v := range interfaceDeserializationFuncs {
if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
deserialisationVersion = v
}
}
if deserialisationVersion == version.Zero {
return nil, NewUnsupportedVersionError("no interface read func for version %s", controllerVersion)
}
return interfaceDeserializationFuncs[deserialisationVersion], nil
}
func readInterfaceList(sourceList []interface{}, readFunc interfaceDeserializationFunc) ([]*interface_, error) {
result := make([]*interface_, 0, len(sourceList))
for i, value := range sourceList {
source, ok := value.(map[string]interface{})
if !ok {
return nil, NewDeserializationError("unexpected value for interface %d, %T", i, value)
}
read, err := readFunc(source)
if err != nil {
return nil, errors.Annotatef(err, "interface %d", i)
}
result = append(result, read)
}
return result, nil
}
type interfaceDeserializationFunc func(map[string]interface{}) (*interface_, error)
var interfaceDeserializationFuncs = map[version.Number]interfaceDeserializationFunc{
twoDotOh: interface_2_0,
}
func interface_2_0(source map[string]interface{}) (*interface_, error) {
fields := schema.Fields{
"resource_uri": schema.String(),
"id": schema.ForceInt(),
"name": schema.String(),
"type": schema.String(),
"enabled": schema.Bool(),
"tags": schema.OneOf(schema.Nil(""), schema.List(schema.String())),
"vlan": schema.OneOf(schema.Nil(""), schema.StringMap(schema.Any())),
"links": schema.List(schema.StringMap(schema.Any())),
"mac_address": schema.OneOf(schema.Nil(""), schema.String()),
"effective_mtu": schema.ForceInt(),
"parents": schema.List(schema.String()),
"children": schema.List(schema.String()),
}
defaults := schema.Defaults{
"mac_address": "",
}
checker := schema.FieldMap(fields, defaults)
coerced, err := checker.Coerce(source, nil)
if err != nil {
return nil, WrapWithDeserializationError(err, "interface 2.0 schema check failed")
}
valid := coerced.(map[string]interface{})
// From here we know that the map returned from the schema coercion
// contains fields of the right type.
var vlan *vlan
// If it's not an attribute map then we know it's nil from the schema check.
if vlanMap, ok := valid["vlan"].(map[string]interface{}); ok {
vlan, err = vlan_2_0(vlanMap)
if err != nil {
return nil, errors.Trace(err)
}
}
links, err := readLinkList(valid["links"].([]interface{}), link_2_0)
if err != nil {
return nil, errors.Trace(err)
}
macAddress, _ := valid["mac_address"].(string)
result := &interface_{
resourceURI: valid["resource_uri"].(string),
id: valid["id"].(int),
name: valid["name"].(string),
type_: valid["type"].(string),
enabled: valid["enabled"].(bool),
tags: convertToStringSlice(valid["tags"]),
vlan: vlan,
links: links,
macAddress: macAddress,
effectiveMTU: valid["effective_mtu"].(int),
parents: convertToStringSlice(valid["parents"]),
children: convertToStringSlice(valid["children"]),
}
return result, nil
}