blob: 64b0d434b0f0c05ba934cc7ec862314a7de6b15e [file] [log] [blame]
David K. Bainbridge3569d622016-09-16 08:40:54 -07001// Copyright 2016 Open Networking Laboratory
2//
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
76 mac_set_arr, err := device["macaddress_set"].GetArray()
77 if len(mac_set_arr) != 1 {
78 return nil, nil, fmt.Errorf("Expecting a single MAC address, recived %d", len(mac_set_arr))
79 }
80
81 mac_obj, err := mac_set_arr[0].GetMap()
82 if err != nil {
83 return nil, nil, err
84 }
85
86 mac, err := mac_obj["mac_address"].GetString()
87 if err != nil {
88 return nil, nil, err
89 }
90 mac = strings.ToUpper(mac)
91 all[i].MAC = mac
92
93 byName[name] = &all[i]
94 byMac[mac] = &all[i]
95 }
96
97 return
98}
99
100// synctoMaas checks to see if the devices is already in MAAS and if not adds it containment is determined by a matching
101// hostname and MAC address. if there is not match then a new devie is POSTed to MAAS
102func (c *AppContext) syncToMaas(request chan []AddressRec) {
103 log.Info("Starting MAAS Switch Synchronizer")
104
105 // Wait for request
106 for list := range request {
107 // Get current device list and convert it to some maps for quick indexing
108 devices := c.maasClient.GetSubObject("devices")
109 deviceObjects, err := devices.CallGet("list", url.Values{})
110 if err != nil {
111 log.Errorf("Unable to synchronize switches to MAAS, unable to get current devices : %s",
112 err)
113 break
114 }
115 deviceList, err := deviceObjects.GetArray()
116 if err != nil {
117 log.Errorf("Unable to synchronize switches to MAAS, unable to deserialize devices : %s",
118 err)
119 break
120 }
121 byName, byMac, err := process(deviceList)
122 if err != nil {
123 log.Errorf("Unable to process current device list : %s", err)
124 return
125 }
126
127 // Get all the subnets from MAAS and store them in a local array for quick access. The subnets
128 // are used to attempt to map the switch into a subnet based on its IP address
129 subnets := c.maasClient.GetSubObject("subnets")
130 subnetObjects, err := subnets.CallGet("", url.Values{})
131 if err != nil {
132 log.Errorf("Unable to retrieve subnets from MAAS : %s", err)
133 return
134 }
135
136 subnetArr, err := subnetObjects.GetArray()
137 if err != nil {
138 log.Errorf("Unable to get subnet array from MAAS : %s", err)
139 return
140 }
141
142 subnetRecs := make([]subnetRec, len(subnetArr))
143 for i, subnetObj := range subnetArr {
144 subnet, err := subnetObj.GetMap()
145 if err != nil {
146 log.Errorf("Unable to process subnet from MAAS : %s", err)
147 return
148 }
149
150 name, err := subnet["name"].GetString()
151 if err != nil {
152 log.Errorf("Unable to get Name from MAAS subnet : %s", err)
153 return
154 }
155
156 s_cidr, err := subnet["cidr"].GetString()
157 if err != nil {
158 log.Errorf("Unable to get CIDR from MAAS subnet : %s", err)
159 return
160 }
161 _, cidr, err := net.ParseCIDR(s_cidr)
162 if err != nil {
163 log.Errorf("Unable to parse CIDR '%s' from MAAS : %s", s_cidr, err)
164 return
165 }
166
167 vlanMap, err := subnet["vlan"].GetMap()
168 if err != nil {
169 log.Errorf("Unable to get vlan for MAAS subnet '%s' : %s", s_cidr, err)
170 return
171 }
172
173 id, err := vlanMap["id"].GetFloat64()
174 if err != nil {
175 log.Errorf("Unable to get VLAN ID for MAAS subnet '%s' : %s", s_cidr, err)
176 return
177 }
178 subnetRecs[i].name = name
179 subnetRecs[i].cidr = cidr
180 subnetRecs[i].vlanID = strconv.Itoa(int(id))
181 }
182
183 // Iterage over the list of devices to sync to MAAS
184 for _, rec := range list {
185 // First check for matching hostname
186 found, ok := byName[rec.Name]
187 if ok {
188 // Found an existing record with a matching hostname. If the MAC matches then
189 // this means this device is already in MAAS and we are good.
190 if strings.ToUpper(rec.MAC) == found.MAC {
191 // All is good
192 log.Infof("Device '%s (%s)' already in MAAS", rec.Name, rec.MAC)
193 continue
194 } else {
195 // Have a matching hostname, but a different MAC address. Can't
196 // push a duplicate hostname to MAAS. As the MAC is considered the
197 // unique identifier we will append the MAC address to the given
198 // hostname and add the device under that host name
199 log.Warnf("Device '%s (%s)' exists in MAAS with a different MAC, augmenting hostname with MAC to form unique hostname",
200 rec.Name, rec.MAC)
201 namePlus := rec.Name + "-" + strings.Replace(strings.ToLower(rec.MAC), ":", "", -1)
202 _, ok = byName[namePlus]
203 if ok {
204 // A record with the name + mac already exists, assume this is the
205 // same record then and all is well
206 log.Infof("Device '%s (%s)' already in MAAS", namePlus, rec.MAC)
207 continue
208 }
209
210 // Modify the record so that it will be created with the new name
211 rec.Name = namePlus
212 }
213 }
214 found, ok = byMac[strings.ToUpper(rec.MAC)]
215 if ok {
216 // Found a record with this MAC address, but a different hostname. Update
217 // the hostname to the correct value
218 log.Infof("Device with matching MAC, but different name found, updating name to '%s (%s)'",
219 rec.Name, rec.MAC)
220 deviceObj := c.maasClient.GetSubObject(found.ResourceUri)
221 _, err := deviceObj.Update(url.Values{
222 "hostname": []string{rec.Name},
223 })
224 if err != nil {
225 log.Errorf("Unable to update hostname for device '%s (%s)' in MAAS : %s",
226 rec.Name, rec.MAC, err)
227 }
228 continue
229 }
230
231 // The device does not currently exist in MAAS, so add it
232 log.Infof("Adding device '%s (%s)' to MAAS", rec.Name, rec.MAC)
233 deviceObj, err := devices.CallPost("new", url.Values{
234 "hostname": []string{rec.Name},
235 "mac_addresses": []string{rec.MAC},
236 })
237 if err != nil {
238 log.Errorf("Unable to synchronize switch '%s' (%s, %s) to MAAS : %s",
239 rec.Name, rec.IP, rec.MAC, err)
240 continue
241 }
242
243 // Get the interface of the device so if can be assigned to a subnet
244 deviceMap, err := deviceObj.GetMap()
245 if err != nil {
246 log.Errorf("Can't get device object for '%s (%s)' : %s",
247 rec.Name, rec.MAC, err)
248 continue
249 }
250
251 interfaceSetArr, err := deviceMap["interface_set"].GetArray()
252 if err != nil {
253 log.Errorf("Can't get device interface set for '%s (%s)' : %s",
254 rec.Name, rec.MAC, err)
255 continue
256 }
257
258 ifaceMap, err := interfaceSetArr[0].GetMap()
259 if err != nil {
260 log.Errorf("Unable to get first interface for '%s (%s)' : %s",
261 rec.Name, rec.MAC, err)
262 continue
263 }
264
265 ifaceUri, err := ifaceMap["resource_uri"].GetString()
266 if err != nil {
267 log.Errorf("Unable to get interface URI for '%s (%s)' : %s",
268 rec.Name, rec.MAC, err)
269 continue
270 }
271
272 // Get the appropriate subnect for the switches IP. If one cannot be found then
273 // nothing can be done
274 subnetRec := getSubnetForAddr(subnetRecs, rec.IP)
275 if subnetRec == nil {
276 log.Errorf("Unable to find VLAN ID for '%s (%s)' using IP '%s'",
277 rec.Name, rec.MAC, rec.IP)
278 continue
279 }
280
281 // If we have a subnet for the device set it back to MAAS
282 _, err = c.maasClient.GetSubObject(ifaceUri).Update(url.Values{
283 "name": []string{"ma1"},
284 "vlan": []string{subnetRec.vlanID},
285 })
286 if err != nil {
287 log.Errorf("Unable to update interface name of '%s (%s)' : %s",
288 rec.Name, rec.MAC, err)
289 }
290 }
291 }
292}