blob: 54386698d178e00aeaf1aa9b14e47b43f4ff3cff [file] [log] [blame]
// Copyright 2012-2016 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package gomaasapi
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"regexp"
"sort"
"strings"
)
func getSubnetsEndpoint(version string) string {
return fmt.Sprintf("/api/%s/subnets/", version)
}
// CreateSubnet is used to receive new subnets via the MAAS API
type CreateSubnet struct {
DNSServers []string `json:"dns_servers"`
Name string `json:"name"`
Space string `json:"space"`
GatewayIP string `json:"gateway_ip"`
CIDR string `json:"cidr"`
// VLAN this subnet belongs to. Currently ignored.
// TODO: Defaults to the default VLAN
// for the provided fabric or defaults to the default VLAN
// in the default fabric.
VLAN *uint `json:"vlan"`
// Fabric for the subnet. Currently ignored.
// TODO: Defaults to the fabric the provided
// VLAN belongs to or defaults to the default fabric.
Fabric *uint `json:"fabric"`
// VID of the VLAN this subnet belongs to. Currently ignored.
// TODO: Only used when vlan
// is not provided. Picks the VLAN with this VID in the provided
// fabric or the default fabric if one is not given.
VID *uint `json:"vid"`
// This is used for updates (PUT) and is ignored by create (POST)
ID uint `json:"id"`
}
// TestSubnet is the MAAS API subnet representation
type TestSubnet struct {
DNSServers []string `json:"dns_servers"`
Name string `json:"name"`
Space string `json:"space"`
VLAN TestVLAN `json:"vlan"`
GatewayIP string `json:"gateway_ip"`
CIDR string `json:"cidr"`
ResourceURI string `json:"resource_uri"`
ID uint `json:"id"`
InUseIPAddresses []IP `json:"-"`
FixedAddressRanges []AddressRange `json:"-"`
}
// AddFixedAddressRange adds an AddressRange to the list of fixed address ranges
// that subnet stores.
func (server *TestServer) AddFixedAddressRange(subnetID uint, ar AddressRange) {
subnet := server.subnets[subnetID]
ar.startUint = IPFromString(ar.Start).UInt64()
ar.endUint = IPFromString(ar.End).UInt64()
subnet.FixedAddressRanges = append(subnet.FixedAddressRanges, ar)
server.subnets[subnetID] = subnet
}
// subnetsHandler handles requests for '/api/<version>/subnets/'.
func subnetsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
var err error
values, err := url.ParseQuery(r.URL.RawQuery)
checkError(err)
op := values.Get("op")
includeRangesString := strings.ToLower(values.Get("include_ranges"))
subnetsURLRE := regexp.MustCompile(`/subnets/(.+?)/`)
subnetsURLMatch := subnetsURLRE.FindStringSubmatch(r.URL.Path)
subnetsURL := getSubnetsEndpoint(server.version)
var ID uint
var gotID bool
if subnetsURLMatch != nil {
ID, err = NameOrIDToID(subnetsURLMatch[1], server.subnetNameToID, 1, uint(len(server.subnets)))
if err != nil {
http.NotFoundHandler().ServeHTTP(w, r)
return
}
gotID = true
}
var includeRanges bool
switch includeRangesString {
case "true", "yes", "1":
includeRanges = true
}
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/vnd.api+json")
if len(server.subnets) == 0 {
// Until a subnet is registered, behave as if the endpoint
// does not exist. This way we can simulate older MAAS
// servers that do not support subnets.
http.NotFoundHandler().ServeHTTP(w, r)
return
}
if r.URL.Path == subnetsURL {
var subnets []TestSubnet
for i := uint(1); i < server.nextSubnet; i++ {
s, ok := server.subnets[i]
if ok {
subnets = append(subnets, s)
}
}
PrettyJsonWriter(subnets, w)
} else if gotID == false {
w.WriteHeader(http.StatusBadRequest)
} else {
switch op {
case "unreserved_ip_ranges":
PrettyJsonWriter(server.subnetUnreservedIPRanges(server.subnets[ID]), w)
case "reserved_ip_ranges":
PrettyJsonWriter(server.subnetReservedIPRanges(server.subnets[ID]), w)
case "statistics":
PrettyJsonWriter(server.subnetStatistics(server.subnets[ID], includeRanges), w)
default:
PrettyJsonWriter(server.subnets[ID], w)
}
}
checkError(err)
case "POST":
server.NewSubnet(r.Body)
case "PUT":
server.UpdateSubnet(r.Body)
case "DELETE":
delete(server.subnets, ID)
w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusBadRequest)
}
}
type addressList []IP
func (a addressList) Len() int { return len(a) }
func (a addressList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a addressList) Less(i, j int) bool { return a[i].UInt64() < a[j].UInt64() }
// AddressRange is used to generate reserved IP address range lists
type AddressRange struct {
Start string `json:"start"`
startUint uint64
End string `json:"end"`
endUint uint64
Purpose []string `json:"purpose,omitempty"`
NumAddresses uint `json:"num_addresses"`
}
// AddressRangeList is a list of AddressRange
type AddressRangeList struct {
ar []AddressRange
}
// Append appends a new AddressRange to an AddressRangeList
func (ranges *AddressRangeList) Append(startIP, endIP IP) {
var i AddressRange
i.Start, i.End = startIP.String(), endIP.String()
i.startUint, i.endUint = startIP.UInt64(), endIP.UInt64()
i.NumAddresses = uint(1 + endIP.UInt64() - startIP.UInt64())
i.Purpose = startIP.Purpose
ranges.ar = append(ranges.ar, i)
}
func appendRangesToIPList(subnet TestSubnet, ipAddresses *[]IP) {
for _, r := range subnet.FixedAddressRanges {
for v := r.startUint; v <= r.endUint; v++ {
ip := IPFromInt64(v)
ip.Purpose = r.Purpose
*ipAddresses = append(*ipAddresses, ip)
}
}
}
func (server *TestServer) subnetUnreservedIPRanges(subnet TestSubnet) []AddressRange {
// Make a sorted copy of subnet.InUseIPAddresses
ipAddresses := make([]IP, len(subnet.InUseIPAddresses))
copy(ipAddresses, subnet.InUseIPAddresses)
appendRangesToIPList(subnet, &ipAddresses)
sort.Sort(addressList(ipAddresses))
// We need the first and last address in the subnet
var ranges AddressRangeList
var startIP, endIP, lastUsableIP IP
_, ipNet, err := net.ParseCIDR(subnet.CIDR)
checkError(err)
startIP = IPFromNetIP(ipNet.IP)
// Start with the lowest usable address in the range, which is 1 above
// what net.ParseCIDR will give back.
startIP.SetUInt64(startIP.UInt64() + 1)
ones, bits := ipNet.Mask.Size()
set := ^((^uint64(0)) << uint(bits-ones))
// The last usable address is one below the broadcast address, which is
// what you get by bitwise ORing 'set' with any IP address in the subnet.
lastUsableIP.SetUInt64((startIP.UInt64() | set) - 1)
for _, endIP = range ipAddresses {
end := endIP.UInt64()
if endIP.UInt64() == startIP.UInt64() {
if endIP.UInt64() != lastUsableIP.UInt64() {
startIP.SetUInt64(end + 1)
}
continue
}
if end == lastUsableIP.UInt64() {
continue
}
ranges.Append(startIP, IPFromInt64(end-1))
startIP.SetUInt64(end + 1)
}
if startIP.UInt64() != lastUsableIP.UInt64() {
ranges.Append(startIP, lastUsableIP)
}
return ranges.ar
}
func (server *TestServer) subnetReservedIPRanges(subnet TestSubnet) []AddressRange {
var ranges AddressRangeList
var startIP, thisIP IP
// Make a sorted copy of subnet.InUseIPAddresses
ipAddresses := make([]IP, len(subnet.InUseIPAddresses))
copy(ipAddresses, subnet.InUseIPAddresses)
appendRangesToIPList(subnet, &ipAddresses)
sort.Sort(addressList(ipAddresses))
if len(ipAddresses) == 0 {
ar := ranges.ar
if ar == nil {
ar = []AddressRange{}
}
return ar
}
startIP = ipAddresses[0]
lastIP := ipAddresses[0]
for _, thisIP = range ipAddresses {
var purposeMissmatch bool
for i, p := range thisIP.Purpose {
if startIP.Purpose[i] != p {
purposeMissmatch = true
}
}
if (thisIP.UInt64() != lastIP.UInt64() && thisIP.UInt64() != lastIP.UInt64()+1) || purposeMissmatch {
ranges.Append(startIP, lastIP)
startIP = thisIP
}
lastIP = thisIP
}
if len(ranges.ar) == 0 || ranges.ar[len(ranges.ar)-1].endUint != lastIP.UInt64() {
ranges.Append(startIP, lastIP)
}
return ranges.ar
}
// SubnetStats holds statistics about a subnet
type SubnetStats struct {
NumAvailable uint `json:"num_available"`
LargestAvailable uint `json:"largest_available"`
NumUnavailable uint `json:"num_unavailable"`
TotalAddresses uint `json:"total_addresses"`
Usage float32 `json:"usage"`
UsageString string `json:"usage_string"`
Ranges []AddressRange `json:"ranges"`
}
func (server *TestServer) subnetStatistics(subnet TestSubnet, includeRanges bool) SubnetStats {
var stats SubnetStats
_, ipNet, err := net.ParseCIDR(subnet.CIDR)
checkError(err)
ones, bits := ipNet.Mask.Size()
stats.TotalAddresses = (1 << uint(bits-ones)) - 2
stats.NumUnavailable = uint(len(subnet.InUseIPAddresses))
stats.NumAvailable = stats.TotalAddresses - stats.NumUnavailable
stats.Usage = float32(stats.NumUnavailable) / float32(stats.TotalAddresses)
stats.UsageString = fmt.Sprintf("%0.1f%%", stats.Usage*100)
// Calculate stats.LargestAvailable - the largest contiguous block of IP addresses available
reserved := server.subnetUnreservedIPRanges(subnet)
for _, addressRange := range reserved {
if addressRange.NumAddresses > stats.LargestAvailable {
stats.LargestAvailable = addressRange.NumAddresses
}
}
if includeRanges {
stats.Ranges = reserved
}
return stats
}
func decodePostedSubnet(subnetJSON io.Reader) CreateSubnet {
var postedSubnet CreateSubnet
decoder := json.NewDecoder(subnetJSON)
err := decoder.Decode(&postedSubnet)
checkError(err)
if postedSubnet.DNSServers == nil {
postedSubnet.DNSServers = []string{}
}
return postedSubnet
}
// UpdateSubnet creates a subnet in the test server
func (server *TestServer) UpdateSubnet(subnetJSON io.Reader) TestSubnet {
postedSubnet := decodePostedSubnet(subnetJSON)
updatedSubnet := subnetFromCreateSubnet(postedSubnet)
server.subnets[updatedSubnet.ID] = updatedSubnet
return updatedSubnet
}
// NewSubnet creates a subnet in the test server
func (server *TestServer) NewSubnet(subnetJSON io.Reader) *TestSubnet {
postedSubnet := decodePostedSubnet(subnetJSON)
newSubnet := subnetFromCreateSubnet(postedSubnet)
newSubnet.ID = server.nextSubnet
server.subnets[server.nextSubnet] = newSubnet
server.subnetNameToID[newSubnet.Name] = newSubnet.ID
server.nextSubnet++
return &newSubnet
}
// NodeNetworkInterface represents a network interface attached to a node
type NodeNetworkInterface struct {
Name string `json:"name"`
Links []NetworkLink `json:"links"`
}
// Node represents a node
type Node struct {
SystemID string `json:"system_id"`
Interfaces []NodeNetworkInterface `json:"interface_set"`
}
// NetworkLink represents a MAAS network link
type NetworkLink struct {
ID uint `json:"id"`
Mode string `json:"mode"`
Subnet *TestSubnet `json:"subnet"`
}
// SetNodeNetworkLink records that the given node + interface are in subnet
func (server *TestServer) SetNodeNetworkLink(SystemID string, nodeNetworkInterface NodeNetworkInterface) {
for i, ni := range server.nodeMetadata[SystemID].Interfaces {
if ni.Name == nodeNetworkInterface.Name {
server.nodeMetadata[SystemID].Interfaces[i] = nodeNetworkInterface
return
}
}
n := server.nodeMetadata[SystemID]
n.Interfaces = append(n.Interfaces, nodeNetworkInterface)
server.nodeMetadata[SystemID] = n
}
// subnetFromCreateSubnet creates a subnet in the test server
func subnetFromCreateSubnet(postedSubnet CreateSubnet) TestSubnet {
var newSubnet TestSubnet
newSubnet.DNSServers = postedSubnet.DNSServers
newSubnet.Name = postedSubnet.Name
newSubnet.Space = postedSubnet.Space
//TODO: newSubnet.VLAN = server.postedSubnetVLAN
newSubnet.GatewayIP = postedSubnet.GatewayIP
newSubnet.CIDR = postedSubnet.CIDR
newSubnet.ID = postedSubnet.ID
return newSubnet
}