David K. Bainbridge | 528b318 | 2017-01-23 08:51:59 -0800 | [diff] [blame^] | 1 | // Copyright 2012-2016 Canonical Ltd. |
| 2 | // Licensed under the LGPLv3, see LICENCE file for details. |
| 3 | |
| 4 | package gomaasapi |
| 5 | |
| 6 | import ( |
| 7 | "encoding/json" |
| 8 | "fmt" |
| 9 | "io" |
| 10 | "net" |
| 11 | "net/http" |
| 12 | "net/url" |
| 13 | "regexp" |
| 14 | "sort" |
| 15 | "strings" |
| 16 | ) |
| 17 | |
| 18 | func getSubnetsEndpoint(version string) string { |
| 19 | return fmt.Sprintf("/api/%s/subnets/", version) |
| 20 | } |
| 21 | |
| 22 | // CreateSubnet is used to receive new subnets via the MAAS API |
| 23 | type CreateSubnet struct { |
| 24 | DNSServers []string `json:"dns_servers"` |
| 25 | Name string `json:"name"` |
| 26 | Space string `json:"space"` |
| 27 | GatewayIP string `json:"gateway_ip"` |
| 28 | CIDR string `json:"cidr"` |
| 29 | |
| 30 | // VLAN this subnet belongs to. Currently ignored. |
| 31 | // TODO: Defaults to the default VLAN |
| 32 | // for the provided fabric or defaults to the default VLAN |
| 33 | // in the default fabric. |
| 34 | VLAN *uint `json:"vlan"` |
| 35 | |
| 36 | // Fabric for the subnet. Currently ignored. |
| 37 | // TODO: Defaults to the fabric the provided |
| 38 | // VLAN belongs to or defaults to the default fabric. |
| 39 | Fabric *uint `json:"fabric"` |
| 40 | |
| 41 | // VID of the VLAN this subnet belongs to. Currently ignored. |
| 42 | // TODO: Only used when vlan |
| 43 | // is not provided. Picks the VLAN with this VID in the provided |
| 44 | // fabric or the default fabric if one is not given. |
| 45 | VID *uint `json:"vid"` |
| 46 | |
| 47 | // This is used for updates (PUT) and is ignored by create (POST) |
| 48 | ID uint `json:"id"` |
| 49 | } |
| 50 | |
| 51 | // TestSubnet is the MAAS API subnet representation |
| 52 | type TestSubnet struct { |
| 53 | DNSServers []string `json:"dns_servers"` |
| 54 | Name string `json:"name"` |
| 55 | Space string `json:"space"` |
| 56 | VLAN TestVLAN `json:"vlan"` |
| 57 | GatewayIP string `json:"gateway_ip"` |
| 58 | CIDR string `json:"cidr"` |
| 59 | |
| 60 | ResourceURI string `json:"resource_uri"` |
| 61 | ID uint `json:"id"` |
| 62 | InUseIPAddresses []IP `json:"-"` |
| 63 | FixedAddressRanges []AddressRange `json:"-"` |
| 64 | } |
| 65 | |
| 66 | // AddFixedAddressRange adds an AddressRange to the list of fixed address ranges |
| 67 | // that subnet stores. |
| 68 | func (server *TestServer) AddFixedAddressRange(subnetID uint, ar AddressRange) { |
| 69 | subnet := server.subnets[subnetID] |
| 70 | ar.startUint = IPFromString(ar.Start).UInt64() |
| 71 | ar.endUint = IPFromString(ar.End).UInt64() |
| 72 | subnet.FixedAddressRanges = append(subnet.FixedAddressRanges, ar) |
| 73 | server.subnets[subnetID] = subnet |
| 74 | } |
| 75 | |
| 76 | // subnetsHandler handles requests for '/api/<version>/subnets/'. |
| 77 | func subnetsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { |
| 78 | var err error |
| 79 | values, err := url.ParseQuery(r.URL.RawQuery) |
| 80 | checkError(err) |
| 81 | op := values.Get("op") |
| 82 | includeRangesString := strings.ToLower(values.Get("include_ranges")) |
| 83 | subnetsURLRE := regexp.MustCompile(`/subnets/(.+?)/`) |
| 84 | subnetsURLMatch := subnetsURLRE.FindStringSubmatch(r.URL.Path) |
| 85 | subnetsURL := getSubnetsEndpoint(server.version) |
| 86 | |
| 87 | var ID uint |
| 88 | var gotID bool |
| 89 | if subnetsURLMatch != nil { |
| 90 | ID, err = NameOrIDToID(subnetsURLMatch[1], server.subnetNameToID, 1, uint(len(server.subnets))) |
| 91 | |
| 92 | if err != nil { |
| 93 | http.NotFoundHandler().ServeHTTP(w, r) |
| 94 | return |
| 95 | } |
| 96 | |
| 97 | gotID = true |
| 98 | } |
| 99 | |
| 100 | var includeRanges bool |
| 101 | switch includeRangesString { |
| 102 | case "true", "yes", "1": |
| 103 | includeRanges = true |
| 104 | } |
| 105 | |
| 106 | switch r.Method { |
| 107 | case "GET": |
| 108 | w.Header().Set("Content-Type", "application/vnd.api+json") |
| 109 | if len(server.subnets) == 0 { |
| 110 | // Until a subnet is registered, behave as if the endpoint |
| 111 | // does not exist. This way we can simulate older MAAS |
| 112 | // servers that do not support subnets. |
| 113 | http.NotFoundHandler().ServeHTTP(w, r) |
| 114 | return |
| 115 | } |
| 116 | |
| 117 | if r.URL.Path == subnetsURL { |
| 118 | var subnets []TestSubnet |
| 119 | for i := uint(1); i < server.nextSubnet; i++ { |
| 120 | s, ok := server.subnets[i] |
| 121 | if ok { |
| 122 | subnets = append(subnets, s) |
| 123 | } |
| 124 | } |
| 125 | PrettyJsonWriter(subnets, w) |
| 126 | } else if gotID == false { |
| 127 | w.WriteHeader(http.StatusBadRequest) |
| 128 | } else { |
| 129 | switch op { |
| 130 | case "unreserved_ip_ranges": |
| 131 | PrettyJsonWriter(server.subnetUnreservedIPRanges(server.subnets[ID]), w) |
| 132 | case "reserved_ip_ranges": |
| 133 | PrettyJsonWriter(server.subnetReservedIPRanges(server.subnets[ID]), w) |
| 134 | case "statistics": |
| 135 | PrettyJsonWriter(server.subnetStatistics(server.subnets[ID], includeRanges), w) |
| 136 | default: |
| 137 | PrettyJsonWriter(server.subnets[ID], w) |
| 138 | } |
| 139 | } |
| 140 | checkError(err) |
| 141 | case "POST": |
| 142 | server.NewSubnet(r.Body) |
| 143 | case "PUT": |
| 144 | server.UpdateSubnet(r.Body) |
| 145 | case "DELETE": |
| 146 | delete(server.subnets, ID) |
| 147 | w.WriteHeader(http.StatusOK) |
| 148 | default: |
| 149 | w.WriteHeader(http.StatusBadRequest) |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | type addressList []IP |
| 154 | |
| 155 | func (a addressList) Len() int { return len(a) } |
| 156 | func (a addressList) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| 157 | func (a addressList) Less(i, j int) bool { return a[i].UInt64() < a[j].UInt64() } |
| 158 | |
| 159 | // AddressRange is used to generate reserved IP address range lists |
| 160 | type AddressRange struct { |
| 161 | Start string `json:"start"` |
| 162 | startUint uint64 |
| 163 | End string `json:"end"` |
| 164 | endUint uint64 |
| 165 | Purpose []string `json:"purpose,omitempty"` |
| 166 | NumAddresses uint `json:"num_addresses"` |
| 167 | } |
| 168 | |
| 169 | // AddressRangeList is a list of AddressRange |
| 170 | type AddressRangeList struct { |
| 171 | ar []AddressRange |
| 172 | } |
| 173 | |
| 174 | // Append appends a new AddressRange to an AddressRangeList |
| 175 | func (ranges *AddressRangeList) Append(startIP, endIP IP) { |
| 176 | var i AddressRange |
| 177 | i.Start, i.End = startIP.String(), endIP.String() |
| 178 | i.startUint, i.endUint = startIP.UInt64(), endIP.UInt64() |
| 179 | i.NumAddresses = uint(1 + endIP.UInt64() - startIP.UInt64()) |
| 180 | i.Purpose = startIP.Purpose |
| 181 | ranges.ar = append(ranges.ar, i) |
| 182 | } |
| 183 | |
| 184 | func appendRangesToIPList(subnet TestSubnet, ipAddresses *[]IP) { |
| 185 | for _, r := range subnet.FixedAddressRanges { |
| 186 | for v := r.startUint; v <= r.endUint; v++ { |
| 187 | ip := IPFromInt64(v) |
| 188 | ip.Purpose = r.Purpose |
| 189 | *ipAddresses = append(*ipAddresses, ip) |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | func (server *TestServer) subnetUnreservedIPRanges(subnet TestSubnet) []AddressRange { |
| 195 | // Make a sorted copy of subnet.InUseIPAddresses |
| 196 | ipAddresses := make([]IP, len(subnet.InUseIPAddresses)) |
| 197 | copy(ipAddresses, subnet.InUseIPAddresses) |
| 198 | appendRangesToIPList(subnet, &ipAddresses) |
| 199 | sort.Sort(addressList(ipAddresses)) |
| 200 | |
| 201 | // We need the first and last address in the subnet |
| 202 | var ranges AddressRangeList |
| 203 | var startIP, endIP, lastUsableIP IP |
| 204 | |
| 205 | _, ipNet, err := net.ParseCIDR(subnet.CIDR) |
| 206 | checkError(err) |
| 207 | startIP = IPFromNetIP(ipNet.IP) |
| 208 | // Start with the lowest usable address in the range, which is 1 above |
| 209 | // what net.ParseCIDR will give back. |
| 210 | startIP.SetUInt64(startIP.UInt64() + 1) |
| 211 | |
| 212 | ones, bits := ipNet.Mask.Size() |
| 213 | set := ^((^uint64(0)) << uint(bits-ones)) |
| 214 | |
| 215 | // The last usable address is one below the broadcast address, which is |
| 216 | // what you get by bitwise ORing 'set' with any IP address in the subnet. |
| 217 | lastUsableIP.SetUInt64((startIP.UInt64() | set) - 1) |
| 218 | |
| 219 | for _, endIP = range ipAddresses { |
| 220 | end := endIP.UInt64() |
| 221 | |
| 222 | if endIP.UInt64() == startIP.UInt64() { |
| 223 | if endIP.UInt64() != lastUsableIP.UInt64() { |
| 224 | startIP.SetUInt64(end + 1) |
| 225 | } |
| 226 | continue |
| 227 | } |
| 228 | |
| 229 | if end == lastUsableIP.UInt64() { |
| 230 | continue |
| 231 | } |
| 232 | |
| 233 | ranges.Append(startIP, IPFromInt64(end-1)) |
| 234 | startIP.SetUInt64(end + 1) |
| 235 | } |
| 236 | |
| 237 | if startIP.UInt64() != lastUsableIP.UInt64() { |
| 238 | ranges.Append(startIP, lastUsableIP) |
| 239 | } |
| 240 | |
| 241 | return ranges.ar |
| 242 | } |
| 243 | |
| 244 | func (server *TestServer) subnetReservedIPRanges(subnet TestSubnet) []AddressRange { |
| 245 | var ranges AddressRangeList |
| 246 | var startIP, thisIP IP |
| 247 | |
| 248 | // Make a sorted copy of subnet.InUseIPAddresses |
| 249 | ipAddresses := make([]IP, len(subnet.InUseIPAddresses)) |
| 250 | copy(ipAddresses, subnet.InUseIPAddresses) |
| 251 | appendRangesToIPList(subnet, &ipAddresses) |
| 252 | sort.Sort(addressList(ipAddresses)) |
| 253 | if len(ipAddresses) == 0 { |
| 254 | ar := ranges.ar |
| 255 | if ar == nil { |
| 256 | ar = []AddressRange{} |
| 257 | } |
| 258 | return ar |
| 259 | } |
| 260 | |
| 261 | startIP = ipAddresses[0] |
| 262 | lastIP := ipAddresses[0] |
| 263 | for _, thisIP = range ipAddresses { |
| 264 | var purposeMissmatch bool |
| 265 | for i, p := range thisIP.Purpose { |
| 266 | if startIP.Purpose[i] != p { |
| 267 | purposeMissmatch = true |
| 268 | } |
| 269 | } |
| 270 | if (thisIP.UInt64() != lastIP.UInt64() && thisIP.UInt64() != lastIP.UInt64()+1) || purposeMissmatch { |
| 271 | ranges.Append(startIP, lastIP) |
| 272 | startIP = thisIP |
| 273 | } |
| 274 | lastIP = thisIP |
| 275 | } |
| 276 | |
| 277 | if len(ranges.ar) == 0 || ranges.ar[len(ranges.ar)-1].endUint != lastIP.UInt64() { |
| 278 | ranges.Append(startIP, lastIP) |
| 279 | } |
| 280 | |
| 281 | return ranges.ar |
| 282 | } |
| 283 | |
| 284 | // SubnetStats holds statistics about a subnet |
| 285 | type SubnetStats struct { |
| 286 | NumAvailable uint `json:"num_available"` |
| 287 | LargestAvailable uint `json:"largest_available"` |
| 288 | NumUnavailable uint `json:"num_unavailable"` |
| 289 | TotalAddresses uint `json:"total_addresses"` |
| 290 | Usage float32 `json:"usage"` |
| 291 | UsageString string `json:"usage_string"` |
| 292 | Ranges []AddressRange `json:"ranges"` |
| 293 | } |
| 294 | |
| 295 | func (server *TestServer) subnetStatistics(subnet TestSubnet, includeRanges bool) SubnetStats { |
| 296 | var stats SubnetStats |
| 297 | _, ipNet, err := net.ParseCIDR(subnet.CIDR) |
| 298 | checkError(err) |
| 299 | |
| 300 | ones, bits := ipNet.Mask.Size() |
| 301 | stats.TotalAddresses = (1 << uint(bits-ones)) - 2 |
| 302 | stats.NumUnavailable = uint(len(subnet.InUseIPAddresses)) |
| 303 | stats.NumAvailable = stats.TotalAddresses - stats.NumUnavailable |
| 304 | stats.Usage = float32(stats.NumUnavailable) / float32(stats.TotalAddresses) |
| 305 | stats.UsageString = fmt.Sprintf("%0.1f%%", stats.Usage*100) |
| 306 | |
| 307 | // Calculate stats.LargestAvailable - the largest contiguous block of IP addresses available |
| 308 | reserved := server.subnetUnreservedIPRanges(subnet) |
| 309 | for _, addressRange := range reserved { |
| 310 | if addressRange.NumAddresses > stats.LargestAvailable { |
| 311 | stats.LargestAvailable = addressRange.NumAddresses |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | if includeRanges { |
| 316 | stats.Ranges = reserved |
| 317 | } |
| 318 | |
| 319 | return stats |
| 320 | } |
| 321 | |
| 322 | func decodePostedSubnet(subnetJSON io.Reader) CreateSubnet { |
| 323 | var postedSubnet CreateSubnet |
| 324 | decoder := json.NewDecoder(subnetJSON) |
| 325 | err := decoder.Decode(&postedSubnet) |
| 326 | checkError(err) |
| 327 | if postedSubnet.DNSServers == nil { |
| 328 | postedSubnet.DNSServers = []string{} |
| 329 | } |
| 330 | return postedSubnet |
| 331 | } |
| 332 | |
| 333 | // UpdateSubnet creates a subnet in the test server |
| 334 | func (server *TestServer) UpdateSubnet(subnetJSON io.Reader) TestSubnet { |
| 335 | postedSubnet := decodePostedSubnet(subnetJSON) |
| 336 | updatedSubnet := subnetFromCreateSubnet(postedSubnet) |
| 337 | server.subnets[updatedSubnet.ID] = updatedSubnet |
| 338 | return updatedSubnet |
| 339 | } |
| 340 | |
| 341 | // NewSubnet creates a subnet in the test server |
| 342 | func (server *TestServer) NewSubnet(subnetJSON io.Reader) *TestSubnet { |
| 343 | postedSubnet := decodePostedSubnet(subnetJSON) |
| 344 | newSubnet := subnetFromCreateSubnet(postedSubnet) |
| 345 | newSubnet.ID = server.nextSubnet |
| 346 | server.subnets[server.nextSubnet] = newSubnet |
| 347 | server.subnetNameToID[newSubnet.Name] = newSubnet.ID |
| 348 | |
| 349 | server.nextSubnet++ |
| 350 | return &newSubnet |
| 351 | } |
| 352 | |
| 353 | // NodeNetworkInterface represents a network interface attached to a node |
| 354 | type NodeNetworkInterface struct { |
| 355 | Name string `json:"name"` |
| 356 | Links []NetworkLink `json:"links"` |
| 357 | } |
| 358 | |
| 359 | // Node represents a node |
| 360 | type Node struct { |
| 361 | SystemID string `json:"system_id"` |
| 362 | Interfaces []NodeNetworkInterface `json:"interface_set"` |
| 363 | } |
| 364 | |
| 365 | // NetworkLink represents a MAAS network link |
| 366 | type NetworkLink struct { |
| 367 | ID uint `json:"id"` |
| 368 | Mode string `json:"mode"` |
| 369 | Subnet *TestSubnet `json:"subnet"` |
| 370 | } |
| 371 | |
| 372 | // SetNodeNetworkLink records that the given node + interface are in subnet |
| 373 | func (server *TestServer) SetNodeNetworkLink(SystemID string, nodeNetworkInterface NodeNetworkInterface) { |
| 374 | for i, ni := range server.nodeMetadata[SystemID].Interfaces { |
| 375 | if ni.Name == nodeNetworkInterface.Name { |
| 376 | server.nodeMetadata[SystemID].Interfaces[i] = nodeNetworkInterface |
| 377 | return |
| 378 | } |
| 379 | } |
| 380 | n := server.nodeMetadata[SystemID] |
| 381 | n.Interfaces = append(n.Interfaces, nodeNetworkInterface) |
| 382 | server.nodeMetadata[SystemID] = n |
| 383 | } |
| 384 | |
| 385 | // subnetFromCreateSubnet creates a subnet in the test server |
| 386 | func subnetFromCreateSubnet(postedSubnet CreateSubnet) TestSubnet { |
| 387 | var newSubnet TestSubnet |
| 388 | newSubnet.DNSServers = postedSubnet.DNSServers |
| 389 | newSubnet.Name = postedSubnet.Name |
| 390 | newSubnet.Space = postedSubnet.Space |
| 391 | //TODO: newSubnet.VLAN = server.postedSubnetVLAN |
| 392 | newSubnet.GatewayIP = postedSubnet.GatewayIP |
| 393 | newSubnet.CIDR = postedSubnet.CIDR |
| 394 | newSubnet.ID = postedSubnet.ID |
| 395 | return newSubnet |
| 396 | } |