blob: 194123676e86bde93c1fa0d5d64d64b6f80ddf0f [file] [log] [blame]
David K. Bainbridgedf9df632016-07-07 18:47:46 -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.
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070014package main
15
16import (
David K. Bainbridge97ee8052016-06-14 00:52:07 -070017 "bytes"
18 "encoding/json"
David K. Bainbridge528b3182017-01-23 08:51:59 -080019 "flag"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070020 "fmt"
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -070021 "github.com/Sirupsen/logrus"
David K. Bainbridge3569d622016-09-16 08:40:54 -070022 "github.com/gorilla/mux"
23 maas "github.com/juju/gomaasapi"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070024 "github.com/kelseyhightower/envconfig"
David K. Bainbridge11850cb2016-10-28 14:05:59 -070025 "io/ioutil"
David K. Bainbridge97ee8052016-06-14 00:52:07 -070026 "net/http"
David K. Bainbridge528b3182017-01-23 08:51:59 -080027 "os"
David K. Bainbridge11850cb2016-10-28 14:05:59 -070028 "regexp"
David K. Bainbridge3569d622016-09-16 08:40:54 -070029 "sync"
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070030 "time"
31)
32
David K. Bainbridge528b3182017-01-23 08:51:59 -080033const appName = "SWITCHQ"
34
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070035type Config struct {
David K. Bainbridge528b3182017-01-23 08:51:59 -080036 VendorsURL string `default:"file:///switchq/vendors.json" envconfig:"VENDORS_URL" desc:"URL that specifies supported vendor OUI information"`
37 AddressURL string `default:"file:///switchq/dhcp_harvest.inc" envconfig:"ADDRESS_URL" desc:"URL of service or file from which to query IP information"`
38 PollInterval string `default:"1m" envconfig:"POLL_INTERVAL" desc:"how often IP information should be queried and processed"`
39 ProvisionTTL string `default:"1h" envconfig:"PROVISION_TTL" desc:"duraction to wait for a provisioning request before considering it failed"`
40 ProvisionURL string `default:"" envconfig:"PROVISION_URL" desc:"URL of provisioning service"`
41 RoleSelectorURL string `default:"" envconfig:"ROLE_SELECTOR_URL" desc:"URL of service to query for switch role"`
42 DefaultRole string `default:"fabric-switch" envconfig:"DEFAULT_ROLE" desc:"default switch role"`
43 Script string `default:"do-ansible" desc:"script to run for provisioniner"`
44 LogLevel string `default:"warning" envconfig:"LOG_LEVEL" desc:"detail level for logging"`
45 LogFormat string `default:"text" envconfig:"LOG_FORMAT" desc:"output format for logging, text or json"`
46 Listen string `default:"" desc:"IP on which to listen for requests"`
47 Port int `default:"4244" desc:"port on which to listen for requests"`
48 MaasURL string `default:"http://localhost/MAAS" envconfig:"MAAS_URL" desc:"connection string for MAAS"`
49 MaasKey string `default:"" envconfig:"MAAS_API_KEY" desc:"API key for MAAS"`
50 ShowApiKey bool `default:"false" envconfig:"MAAS_SHOW_API_KEY" desc:"display API key in log"`
51 ApiKeyFile string `default:"/secrets/maas_api_key" envconfig:"MAAS_API_KEY_FILE" desc:"file from which to read API key"`
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070052
David K. Bainbridge97ee8052016-06-14 00:52:07 -070053 vendors Vendors
David K. Bainbridgef694f5a2016-06-10 16:21:27 -070054 addressSource AddressSource
55 interval time.Duration
56 ttl time.Duration
57}
58
David K. Bainbridge52f29542016-07-27 22:28:15 -070059const (
60 Pending TaskStatus = iota
61 Running
62 Complete
63 Failed
64)
65
66type RequestInfo struct {
67 Id string `json:"id"`
68 Name string `json:"name"`
69 Ip string `json:"ip"`
70 Mac string `json:"mac"`
71 RoleSelector string `json:"role_selector"`
72 Role string `json:"role"`
73 Script string `json:"script"`
74}
75
76type TaskStatus uint8
77
78type WorkRequest struct {
79 Info *RequestInfo
80 Script string
81 Role string
82}
83
84type StatusMsg struct {
85 Request *WorkRequest `json:"request"`
86 Worker int `json:"worker"`
87 Status TaskStatus `json:"status"`
88 Message string `json:"message"`
89 Timestamp int64 `json:"timestamp"`
90}
91
David K. Bainbridge3569d622016-09-16 08:40:54 -070092type AppContext struct {
93 config Config
94
95 maasClient *maas.MAASObject
96 pushChan chan []AddressRec
97 mutex sync.RWMutex
98 nextList []AddressRec
99 publishList []AddressRec
100}
101
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700102func checkError(err error, msg string, args ...interface{}) {
103 if err != nil {
104 log.Fatalf(msg, args...)
105 }
106}
107
David K. Bainbridge3569d622016-09-16 08:40:54 -0700108func (c *AppContext) getProvisionedState(rec AddressRec) (*StatusMsg, error) {
109 if len(c.config.ProvisionURL) == 0 {
110 log.Warnf("Unable to fetch provisioning state of device '%s' (%s, %s) as no URL for the provisioner was specified",
111 rec.Name, rec.IP, rec.MAC)
112 return nil, fmt.Errorf("No URL for provisioner specified")
113 }
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700114 log.Debugf("Fetching provisioned state of device '%s' (%s, %s)",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700115 rec.Name, rec.IP, rec.MAC)
David K. Bainbridge3569d622016-09-16 08:40:54 -0700116 resp, err := http.Get(c.config.ProvisionURL + rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700117 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700118 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700119 rec.Name, rec.IP, rec.MAC, err)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700120 return nil, err
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700121 }
122 if resp.StatusCode != 404 && int(resp.StatusCode/100) != 2 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700123 log.Errorf("Error while retrieving provisioning state for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700124 rec.Name, rec.IP, rec.MAC, resp.Status)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700125 return nil, fmt.Errorf(resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700126 }
127 defer resp.Body.Close()
128 if resp.StatusCode != 404 {
129 decoder := json.NewDecoder(resp.Body)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700130 var status StatusMsg
131 err = decoder.Decode(&status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700132 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700133 log.Errorf("Unmarshal provisioning service response for device '%s (%s, %s)' : %s",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700134 rec.Name, rec.IP, rec.MAC, err)
David K. Bainbridge52f29542016-07-27 22:28:15 -0700135 return nil, err
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700136 }
David K. Bainbridge52f29542016-07-27 22:28:15 -0700137 return &status, nil
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700138 }
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700139
140 // If we end up here that means that no record was found in the provisioning, so return
141 // a status of -1, w/o an error
David K. Bainbridge52f29542016-07-27 22:28:15 -0700142 return nil, nil
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700143}
144
David K. Bainbridge3569d622016-09-16 08:40:54 -0700145func (c *AppContext) provision(rec AddressRec) error {
146 if len(c.config.ProvisionURL) == 0 {
147 log.Warnf("Unable to POST to provisioner for device '%s' (%s, %s) as no URL for the provisioner was specified",
148 rec.Name, rec.IP, rec.MAC)
149 return fmt.Errorf("No URL for provisioner specified")
150 }
151 log.Infof("POSTing to '%s' for provisioning of '%s (%s)'", c.config.ProvisionURL, rec.Name, rec.MAC)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700152 data := map[string]string{
153 "id": rec.MAC,
154 "name": rec.Name,
155 "ip": rec.IP,
156 "mac": rec.MAC,
157 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700158 if c.config.RoleSelectorURL != "" {
159 data["role_selector"] = c.config.RoleSelectorURL
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700160 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700161 if c.config.DefaultRole != "" {
162 data["role"] = c.config.DefaultRole
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700163 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700164 if c.config.Script != "" {
165 data["script"] = c.config.Script
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700166 }
167
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700168 hc := http.Client{}
169 var b []byte
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700170 b, err := json.Marshal(data)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700171 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700172 log.Errorf("Unable to marshal provisioning data : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700173 return err
174 }
David K. Bainbridge3569d622016-09-16 08:40:54 -0700175 req, err := http.NewRequest("POST", c.config.ProvisionURL, bytes.NewReader(b))
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700176 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700177 log.Errorf("Unable to construct POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700178 return err
179 }
180
181 req.Header.Add("Content-Type", "application/json")
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700182 resp, err := hc.Do(req)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700183 if err != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700184 log.Errorf("Unable to POST request to provisioner : %s", err)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700185 return err
186 }
187
188 defer resp.Body.Close()
189 if resp.StatusCode != http.StatusAccepted {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700190 log.Errorf("Provisioning request not accepted by provisioner : %s", resp.Status)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700191 return err
192 }
193
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700194 return nil
195}
196
David K. Bainbridge3569d622016-09-16 08:40:54 -0700197func (c *AppContext) processRecord(rec AddressRec) error {
198 ok, err := c.config.vendors.Switchq(rec.MAC)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700199 if err != nil {
200 return fmt.Errorf("unable to determine ventor of MAC '%s' (%s)", rec.MAC, err)
201 }
202
203 if !ok {
204 // Not something we care about
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700205 log.Debugf("host with IP '%s' and MAC '%s' and named '%s' not a known switch type",
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700206 rec.IP, rec.MAC, rec.Name)
207 return nil
208 }
209
David K. Bainbridge3569d622016-09-16 08:40:54 -0700210 // Add this IP information to our list of known switches
211 c.nextList = append(c.nextList, rec)
212
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700213 // Verify if the provision status of the node is complete, if in an error state then TTL means
214 // nothing
David K. Bainbridge52f29542016-07-27 22:28:15 -0700215 state, err := c.getProvisionedState(rec)
216 if state != nil {
217 switch state.Status {
218 case Pending, Running: // Pending or Running
219 log.Debugf("device '%s' (%s, %s) is being provisioned",
220 rec.Name, rec.IP, rec.MAC)
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700221 return nil
David K. Bainbridge52f29542016-07-27 22:28:15 -0700222 case Complete: // Complete
223 log.Debugf("device '%s' (%s, %s) has completed provisioning",
224 rec.Name, rec.IP, rec.MAC)
225 case Failed: // Failed
226 log.Debugf("device '%s' (%s, %s) failed last provisioning with message '%s', reattempt",
227 rec.Name, rec.IP, rec.MAC, state.Message)
David K. Bainbridge98bbc042016-08-22 17:35:28 -0700228 state = nil
David K. Bainbridge52f29542016-07-27 22:28:15 -0700229 default: // Unknown state
230 log.Debugf("device '%s' (%s, %s) has unknown provisioning state '%d', will provision",
231 rec.Name, rec.IP, rec.MAC, state.Status)
David K. Bainbridge98bbc042016-08-22 17:35:28 -0700232 state = nil
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700233 }
David K. Bainbridge52f29542016-07-27 22:28:15 -0700234 } else {
235 log.Debugf("device '%s' (%s, %s) has no provisioning record",
236 rec.Name, rec.IP, rec.MAC)
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700237 }
238
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700239 // If TTL is 0 then we will only provision a switch once.
David K. Bainbridge3569d622016-09-16 08:40:54 -0700240 if state == nil || (c.config.ttl > 0 && time.Since(time.Unix(state.Timestamp, 0)) > c.config.ttl) {
David K. Bainbridge52f29542016-07-27 22:28:15 -0700241 if state != nil {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700242 log.Debugf("device '%s' (%s, %s) TTL expired, reprovisioning",
David K. Bainbridgec809ef72016-06-22 21:18:07 -0700243 rec.Name, rec.IP, rec.MAC)
244 }
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700245 c.provision(rec)
David K. Bainbridge3569d622016-09-16 08:40:54 -0700246 } else if c.config.ttl == 0 {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700247 log.Debugf("device '%s' (%s, %s) has completed its one time provisioning, with a TTL set to %s",
David K. Bainbridge3569d622016-09-16 08:40:54 -0700248 rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
David K. Bainbridge97ee8052016-06-14 00:52:07 -0700249 } else {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700250 log.Debugf("device '%s' (%s, %s) has completed provisioning within the specified TTL of %s",
David K. Bainbridge3569d622016-09-16 08:40:54 -0700251 rec.Name, rec.IP, rec.MAC, c.config.ProvisionTTL)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700252 }
253 return nil
254}
255
David K. Bainbridge3569d622016-09-16 08:40:54 -0700256func (c *AppContext) processLoop() {
257 // We use two methods to attempt to find the MAC (hardware) address associated with an IP. The first
258 // is to look in the table. The second is to send an ARP packet.
259 for {
260 log.Infof("Checking for switches @ %s", time.Now())
261 addresses, err := c.config.addressSource.GetAddresses()
262
263 if err != nil {
264 log.Errorf("unable to read addresses from address source : %s", err)
265 } else {
266 log.Infof("Queried %d addresses from address source", len(addresses))
267
268 c.nextList = make([]AddressRec, 0, len(addresses))
269 for _, rec := range addresses {
270 log.Debugf("Processing %s(%s, %s)", rec.Name, rec.IP, rec.MAC)
271 if err := c.processRecord(rec); err != nil {
272 log.Errorf("Error when processing IP '%s' : %s", rec.IP, err)
273 }
274 }
275 c.mutex.Lock()
276 c.publishList = c.nextList
277 c.nextList = nil
278 c.mutex.Unlock()
279 c.pushChan <- c.publishList
280 }
281
282 time.Sleep(c.config.interval)
283 }
284}
285
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700286var log = logrus.New()
David K. Bainbridge528b3182017-01-23 08:51:59 -0800287var appFlags = flag.NewFlagSet("", flag.ContinueOnError)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700288
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700289func main() {
290
291 var err error
David K. Bainbridge3569d622016-09-16 08:40:54 -0700292 context := &AppContext{}
David K. Bainbridge528b3182017-01-23 08:51:59 -0800293
294 appFlags.Usage = func() {
295 envconfig.Usage(appName, &(context.config))
296 }
297 if err := appFlags.Parse(os.Args[1:]); err != nil {
298 if err != flag.ErrHelp {
299 os.Exit(1)
300 } else {
301 return
302 }
303 }
304 err = envconfig.Process(appName, &context.config)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700305 if err != nil {
306 log.Fatalf("Unable to parse configuration options : %s", err)
307 }
308
David K. Bainbridge3569d622016-09-16 08:40:54 -0700309 switch context.config.LogFormat {
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700310 case "json":
311 log.Formatter = &logrus.JSONFormatter{}
312 default:
313 log.Formatter = &logrus.TextFormatter{
314 FullTimestamp: true,
315 ForceColors: true,
316 }
317 }
318
David K. Bainbridge3569d622016-09-16 08:40:54 -0700319 level, err := logrus.ParseLevel(context.config.LogLevel)
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700320 if err != nil {
321 level = logrus.WarnLevel
322 }
323 log.Level = level
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700324
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700325 re := regexp.MustCompile("[^:]")
326 pubKey := context.config.MaasKey
327 if !context.config.ShowApiKey {
328 pubKey = re.ReplaceAllString(context.config.MaasKey, "X")
329 }
330
David K. Bainbridgea9c2e0a2016-07-01 18:33:50 -0700331 log.Infof(`Configuration:
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700332 VENDORS_URL: %s
333 POLL_INTERVAL: %s
334 ADDRESS_URL: %s
335 PROVISION_TTL: %s
336 PROVISION_URL: %s
337 ROLE_SELECTOR_URL: %s
338 DEFAULT_ROLE: %s
339 SCRIPT: %s
340 LISTEN: %s
341 PORT: %d
342 MAAS_URL: %s
343 MAAS_SHOW_API_KEY %t
344 MAAS_API_KEY: %s
345 MAAS_API_KEY_FILE: %s
346 LOG_LEVEL: %s
347 LOG_FORMAT: %s`,
David K. Bainbridge3569d622016-09-16 08:40:54 -0700348 context.config.VendorsURL, context.config.PollInterval, context.config.AddressURL, context.config.ProvisionTTL,
349 context.config.ProvisionURL, context.config.RoleSelectorURL, context.config.DefaultRole, context.config.Script,
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700350 context.config.Listen, context.config.Port, context.config.MaasURL, context.config.ShowApiKey, pubKey,
351 context.config.ApiKeyFile, context.config.LogLevel, context.config.LogFormat)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700352
David K. Bainbridge3569d622016-09-16 08:40:54 -0700353 context.config.vendors, err = NewVendors(context.config.VendorsURL)
354 checkError(err, "Unable to create known vendors list from specified URL '%s' : %s", context.config.VendorsURL, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700355
David K. Bainbridge3569d622016-09-16 08:40:54 -0700356 context.config.addressSource, err = NewAddressSource(context.config.AddressURL)
357 checkError(err, "Unable to create required address source for specified URL '%s' : %s", context.config.AddressURL, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700358
David K. Bainbridge3569d622016-09-16 08:40:54 -0700359 context.config.interval, err = time.ParseDuration(context.config.PollInterval)
360 checkError(err, "Unable to parse specified poll interface '%s' : %s", context.config.PollInterval, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700361
David K. Bainbridge3569d622016-09-16 08:40:54 -0700362 context.config.ttl, err = time.ParseDuration(context.config.ProvisionTTL)
363 checkError(err, "Unable to parse specified provision TTL value of '%s' : %s", context.config.ProvisionTTL, err)
364
David K. Bainbridge11850cb2016-10-28 14:05:59 -0700365 // Attempt to load the API key from a file if it was not set via the environment
366 // and if the file exists
367 if context.config.MaasKey == "" {
368 log.Debugf("Attempting to read MAAS API key from file '%s', because it was not set via environment", context.config.ApiKeyFile)
369 keyBytes, err := ioutil.ReadFile(context.config.ApiKeyFile)
370 if err != nil {
371 log.Warnf("Failed to read MAAS API key from file '%s', was the file mounted as a volume? : %s ",
372 context.config.ApiKeyFile, err)
373 } else {
374 context.config.MaasKey = string(keyBytes)
375 if context.config.ShowApiKey {
376 pubKey = context.config.MaasKey
377 } else {
378 pubKey = re.ReplaceAllString(context.config.MaasKey, "X")
379 }
380 }
381 }
382
David K. Bainbridge3569d622016-09-16 08:40:54 -0700383 if len(context.config.MaasURL) > 0 {
384
385 // Attempt to connect to MAAS
386 authClient, err := maas.NewAuthenticatedClient(context.config.MaasURL, context.config.MaasKey, "1.0")
387 checkError(err, "Unable to connect to MAAS at '%s' : %s", context.config.MaasURL, err)
388
389 context.maasClient = maas.NewMAAS(*authClient)
390 }
391
392 context.pushChan = make(chan []AddressRec, 1)
393
394 go context.processLoop()
395 go context.syncToMaas(context.pushChan)
396
397 router := mux.NewRouter()
398 router.HandleFunc("/switch/", context.ListSwitchesHandler).Methods("GET")
399 http.Handle("/", router)
400 log.Infof("Listening for HTTP request on '%s:%d'", context.config.Listen, context.config.Port)
401 err = http.ListenAndServe(fmt.Sprintf("%s:%d", context.config.Listen, context.config.Port), nil)
402 if err != nil {
403 checkError(err, "Error while attempting to listen to REST requests on '%s:%d' : %s",
404 context.config.Listen, context.config.Port, err)
David K. Bainbridgef694f5a2016-06-10 16:21:27 -0700405 }
406}