blob: 1e9e03560747c47a8e16875b4c743767918e31fd [file] [log] [blame]
Brian O'Connor6a37ea92017-08-03 22:45:59 -07001// Copyright 2016 Open Networking Foundation
David K. Bainbridge3569d622016-09-16 08:40:54 -07002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package main
15
16import (
17 "fmt"
18 maas "github.com/juju/gomaasapi"
19 "net"
20 "net/url"
21 "strconv"
22 "strings"
23)
24
25type MatchRec struct {
26 AddressRec
27 ResourceUri string
28}
29
30type subnetRec struct {
31 name string
32 cidr *net.IPNet
33 vlanID string
34}
35
36func getSubnetForAddr(subnets []subnetRec, ip string) *subnetRec {
37 asIp := net.ParseIP(ip)
38 for _, rec := range subnets {
39 if rec.cidr.Contains(asIp) {
40 return &rec
41 }
42 }
43 return nil
44}
45
46// process converts the device list from MAAS into a maps to quickly lookup a record by name
47func process(deviceList []maas.JSONObject) (byName map[string]*MatchRec, byMac map[string]*MatchRec, err error) {
48 byName = make(map[string]*MatchRec)
49 byMac = make(map[string]*MatchRec)
50 all := make([]MatchRec, len(deviceList))
51
52 for i, deviceObj := range deviceList {
53 device, err := deviceObj.GetMap()
54 if err != nil {
55 return nil, nil, err
56 }
57
58 uri, err := device["resource_uri"].GetString()
59 if err != nil {
60 return nil, nil, err
61 }
62 all[i].ResourceUri = uri
63
64 name, err := device["hostname"].GetString()
65 if err != nil {
66 return nil, nil, err
67 }
68
69 // Strip the domain from the hostname
70 idx := strings.Index(name, ".")
71 if idx != -1 {
72 name = name[:idx]
73 }
74 all[i].Name = name
75
Cem Turkerf203f932018-02-21 23:42:32 +000076 iface_arr, err := device["interface_set"].GetArray()
77 if len(iface_arr) != 1 {
78 return nil, nil, fmt.Errorf("Expecting a single interface, recived %d", len(iface_arr))
David K. Bainbridge3569d622016-09-16 08:40:54 -070079 }
80
Cem Turkerf203f932018-02-21 23:42:32 +000081 iface_obj, err := iface_arr[0].GetMap()
David K. Bainbridge3569d622016-09-16 08:40:54 -070082 if err != nil {
83 return nil, nil, err
84 }
85
Cem Turkerf203f932018-02-21 23:42:32 +000086 mac, err := iface_obj["mac_address"].GetString()
David K. Bainbridge3569d622016-09-16 08:40:54 -070087 if err != nil {
88 return nil, nil, err
89 }
Cem Turkerf203f932018-02-21 23:42:32 +000090
David K. Bainbridge3569d622016-09-16 08:40:54 -070091 mac = strings.ToUpper(mac)
92 all[i].MAC = mac
93
94 byName[name] = &all[i]
95 byMac[mac] = &all[i]
96 }
97
98 return
99}
100
101// synctoMaas checks to see if the devices is already in MAAS and if not adds it containment is determined by a matching
102// hostname and MAC address. if there is not match then a new devie is POSTed to MAAS
103func (c *AppContext) syncToMaas(request chan []AddressRec) {
104 log.Info("Starting MAAS Switch Synchronizer")
105
106 // Wait for request
107 for list := range request {
108 // Get current device list and convert it to some maps for quick indexing
109 devices := c.maasClient.GetSubObject("devices")
Cem Turkerf203f932018-02-21 23:42:32 +0000110 deviceObjects, err := devices.CallGet("", url.Values{})
David K. Bainbridge3569d622016-09-16 08:40:54 -0700111 if err != nil {
112 log.Errorf("Unable to synchronize switches to MAAS, unable to get current devices : %s",
113 err)
114 break
115 }
116 deviceList, err := deviceObjects.GetArray()
117 if err != nil {
118 log.Errorf("Unable to synchronize switches to MAAS, unable to deserialize devices : %s",
119 err)
120 break
121 }
122 byName, byMac, err := process(deviceList)
123 if err != nil {
124 log.Errorf("Unable to process current device list : %s", err)
125 return
126 }
127
128 // Get all the subnets from MAAS and store them in a local array for quick access. The subnets
129 // are used to attempt to map the switch into a subnet based on its IP address
130 subnets := c.maasClient.GetSubObject("subnets")
131 subnetObjects, err := subnets.CallGet("", url.Values{})
132 if err != nil {
133 log.Errorf("Unable to retrieve subnets from MAAS : %s", err)
134 return
135 }
136
137 subnetArr, err := subnetObjects.GetArray()
138 if err != nil {
139 log.Errorf("Unable to get subnet array from MAAS : %s", err)
140 return
141 }
142
143 subnetRecs := make([]subnetRec, len(subnetArr))
144 for i, subnetObj := range subnetArr {
145 subnet, err := subnetObj.GetMap()
146 if err != nil {
147 log.Errorf("Unable to process subnet from MAAS : %s", err)
148 return
149 }
150
151 name, err := subnet["name"].GetString()
152 if err != nil {
153 log.Errorf("Unable to get Name from MAAS subnet : %s", err)
154 return
155 }
156
157 s_cidr, err := subnet["cidr"].GetString()
158 if err != nil {
159 log.Errorf("Unable to get CIDR from MAAS subnet : %s", err)
160 return
161 }
162 _, cidr, err := net.ParseCIDR(s_cidr)
163 if err != nil {
164 log.Errorf("Unable to parse CIDR '%s' from MAAS : %s", s_cidr, err)
165 return
166 }
167
168 vlanMap, err := subnet["vlan"].GetMap()
169 if err != nil {
170 log.Errorf("Unable to get vlan for MAAS subnet '%s' : %s", s_cidr, err)
171 return
172 }
173
174 id, err := vlanMap["id"].GetFloat64()
175 if err != nil {
176 log.Errorf("Unable to get VLAN ID for MAAS subnet '%s' : %s", s_cidr, err)
177 return
178 }
179 subnetRecs[i].name = name
180 subnetRecs[i].cidr = cidr
181 subnetRecs[i].vlanID = strconv.Itoa(int(id))
182 }
183
184 // Iterage over the list of devices to sync to MAAS
185 for _, rec := range list {
186 // First check for matching hostname
187 found, ok := byName[rec.Name]
188 if ok {
189 // Found an existing record with a matching hostname. If the MAC matches then
190 // this means this device is already in MAAS and we are good.
191 if strings.ToUpper(rec.MAC) == found.MAC {
192 // All is good
193 log.Infof("Device '%s (%s)' already in MAAS", rec.Name, rec.MAC)
194 continue
195 } else {
196 // Have a matching hostname, but a different MAC address. Can't
197 // push a duplicate hostname to MAAS. As the MAC is considered the
198 // unique identifier we will append the MAC address to the given
199 // hostname and add the device under that host name
200 log.Warnf("Device '%s (%s)' exists in MAAS with a different MAC, augmenting hostname with MAC to form unique hostname",
201 rec.Name, rec.MAC)
202 namePlus := rec.Name + "-" + strings.Replace(strings.ToLower(rec.MAC), ":", "", -1)
203 _, ok = byName[namePlus]
204 if ok {
205 // A record with the name + mac already exists, assume this is the
206 // same record then and all is well
207 log.Infof("Device '%s (%s)' already in MAAS", namePlus, rec.MAC)
208 continue
209 }
210
211 // Modify the record so that it will be created with the new name
212 rec.Name = namePlus
213 }
214 }
215 found, ok = byMac[strings.ToUpper(rec.MAC)]
216 if ok {
217 // Found a record with this MAC address, but a different hostname. Update
218 // the hostname to the correct value
219 log.Infof("Device with matching MAC, but different name found, updating name to '%s (%s)'",
220 rec.Name, rec.MAC)
221 deviceObj := c.maasClient.GetSubObject(found.ResourceUri)
222 _, err := deviceObj.Update(url.Values{
223 "hostname": []string{rec.Name},
224 })
225 if err != nil {
226 log.Errorf("Unable to update hostname for device '%s (%s)' in MAAS : %s",
227 rec.Name, rec.MAC, err)
228 }
229 continue
230 }
231
232 // The device does not currently exist in MAAS, so add it
233 log.Infof("Adding device '%s (%s)' to MAAS", rec.Name, rec.MAC)
Cem Turkerf203f932018-02-21 23:42:32 +0000234 deviceObj, err := devices.CallPost("", url.Values{
David K. Bainbridge3569d622016-09-16 08:40:54 -0700235 "hostname": []string{rec.Name},
236 "mac_addresses": []string{rec.MAC},
237 })
238 if err != nil {
239 log.Errorf("Unable to synchronize switch '%s' (%s, %s) to MAAS : %s",
240 rec.Name, rec.IP, rec.MAC, err)
241 continue
242 }
243
244 // Get the interface of the device so if can be assigned to a subnet
245 deviceMap, err := deviceObj.GetMap()
246 if err != nil {
247 log.Errorf("Can't get device object for '%s (%s)' : %s",
248 rec.Name, rec.MAC, err)
249 continue
250 }
251
252 interfaceSetArr, err := deviceMap["interface_set"].GetArray()
253 if err != nil {
254 log.Errorf("Can't get device interface set for '%s (%s)' : %s",
255 rec.Name, rec.MAC, err)
256 continue
257 }
258
259 ifaceMap, err := interfaceSetArr[0].GetMap()
260 if err != nil {
261 log.Errorf("Unable to get first interface for '%s (%s)' : %s",
262 rec.Name, rec.MAC, err)
263 continue
264 }
265
266 ifaceUri, err := ifaceMap["resource_uri"].GetString()
267 if err != nil {
268 log.Errorf("Unable to get interface URI for '%s (%s)' : %s",
269 rec.Name, rec.MAC, err)
270 continue
271 }
272
273 // Get the appropriate subnect for the switches IP. If one cannot be found then
274 // nothing can be done
275 subnetRec := getSubnetForAddr(subnetRecs, rec.IP)
276 if subnetRec == nil {
277 log.Errorf("Unable to find VLAN ID for '%s (%s)' using IP '%s'",
278 rec.Name, rec.MAC, rec.IP)
279 continue
280 }
281
282 // If we have a subnet for the device set it back to MAAS
283 _, err = c.maasClient.GetSubObject(ifaceUri).Update(url.Values{
284 "name": []string{"ma1"},
285 "vlan": []string{subnetRec.vlanID},
286 })
287 if err != nil {
288 log.Errorf("Unable to update interface name of '%s (%s)' : %s",
289 rec.Name, rec.MAC, err)
290 }
291 }
292 }
293}