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