blob: abecae7de02685dd39ead405df8a6d5a5046526e [file] [log] [blame]
Author Namea594e632018-08-10 11:33:58 -04001/*
2 Copyright 2017 the original author or authors.
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17package main
18
19import (
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040020 "context"
21 "flag"
22 "fmt"
Author Namea594e632018-08-10 11:33:58 -040023 "log"
Author Namea594e632018-08-10 11:33:58 -040024 "runtime/debug"
donNewtonAlphab3279ea2018-09-18 15:55:32 -040025 "strings"
Author Namea594e632018-08-10 11:33:58 -040026
27 "gerrit.opencord.org/abstract-olt/api"
Author Namea594e632018-08-10 11:33:58 -040028 "google.golang.org/grpc"
29 "google.golang.org/grpc/credentials"
30)
31
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040032func main() {
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040033 /* COMMAND FLAGS */
donNewtonAlphab3279ea2018-09-18 15:55:32 -040034 echo := flag.Bool("e", false, "echo")
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040035 create := flag.Bool("c", false, "create?")
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -040036 update := flag.Bool("u", false, "update?")
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040037 addOlt := flag.Bool("s", false, "addOlt?")
38 provOnt := flag.Bool("o", false, "provisionOnt?")
Don Newton281e0252018-10-22 14:38:50 -040039 provOntFull := flag.Bool("f", false, "provsionOntFull?")
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040040 deleteOnt := flag.Bool("d", false, "deleteOnt")
Don Newton281e0252018-10-22 14:38:50 -040041 output := flag.Bool("output", false, "dump output")
Don Newtone973d342018-10-26 16:44:12 -040042 reflow := flag.Bool("reflow", false, "reflow provisioning tosca")
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040043 /* END COMMAND FLAGS */
44
45 /* CREATE CHASSIS FLAGS */
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -040046 xosUser := flag.String("xos_user", "", "xos_user")
47 xosPassword := flag.String("xos_password", "", "xos_password")
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040048 xosAddress := flag.String("xos_address", "", "xos address")
49 xosPort := flag.Uint("xos_port", 0, "xos port")
50 rack := flag.Uint("rack", 1, "rack number for chassis")
51 shelf := flag.Uint("shelf", 1, "shelf number for chassis")
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040052 /* END CREATE CHASSIS FLAGS */
53
54 /* ADD OLT FLAGS */
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040055 oltAddress := flag.String("olt_address", "", "ip address for olt chassis")
56 oltPort := flag.Uint("olt_port", 0, "listen port for olt chassis")
57 name := flag.String("name", "", "friendly name for olt chassis")
58 driver := flag.String("driver", "", "driver to use with olt chassis")
59 oltType := flag.String("type", "", "olt chassis type")
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040060 /* END ADD OLT FLAGS */
61
62 /* PROVISION / DELETE ONT FLAGS */
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040063 slot := flag.Uint("slot", 1, "slot number 1-16 to provision ont to")
64 port := flag.Uint("port", 1, "port number 1-16 to provision ont to")
65 ont := flag.Uint("ont", 1, "ont number 1-64")
66 serial := flag.String("serial", "", "serial number of ont")
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040067 /* END PROVISION / DELETE ONT FLAGS */
68
Don Newton281e0252018-10-22 14:38:50 -040069 /*PROVISION ONT FULL EXTRA FLAGS*/
70 stag := flag.Uint("stag", 0, "s-tag for ont")
71 ctag := flag.Uint("ctag", 0, "c-tag for ont")
72 nasPort := flag.String("nas_port", "", "NasPortID for ont")
73 circuitID := flag.String("circuit_id", "", "CircuitID for ont")
74 /*END PROVISION ONT FULL EXTRA FLAGS*/
75
76 /* ECHO FLAGS */
77 message := flag.String("message", "ping", "message to be echoed back")
78 /*END ECHO FLAGS*/
79
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040080 /*GENERIC FLAGS */
81 clli := flag.String("clli", "", "clli of abstract chassis")
donNewtonAlphab3279ea2018-09-18 15:55:32 -040082 useSsl := flag.Bool("ssl", false, "use ssl")
83 useAuth := flag.Bool("auth", false, "use auth")
84 crtFile := flag.String("cert", "cert/server.crt", "Public cert for server to establish tls session")
85 serverAddressPort := flag.String("server", "localhost:7777", "address and port of AbstractOLT server")
86 fqdn := flag.String("fqdn", "", "FQDN of the service to match what is in server.crt")
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040087 /*GENERIC FLAGS */
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040088
89 flag.Parse()
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040090
donNewtonAlphab3279ea2018-09-18 15:55:32 -040091 if *useSsl {
92 if *fqdn == "" {
93 fqdn = &(strings.Split(*serverAddressPort, ":")[0])
94 fmt.Printf("using %s as the FQDN for the AbstractOLT server", *fqdn)
95 }
96 }
donNewtonAlpha1d2d6812018-09-14 16:00:02 -040097
Don Newtone973d342018-10-26 16:44:12 -040098 cmdFlags := []*bool{echo, addOlt, update, create, provOnt, provOntFull, deleteOnt, output, reflow}
donNewtonAlphae7ab5b92018-09-27 15:09:14 -040099 cmdCount := 0
100 for _, flag := range cmdFlags {
101 if *flag {
102 cmdCount++
103 }
104 }
105 if cmdCount > 1 {
106 fmt.Println("CMD You can only call one method at a time")
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400107 usage()
108 return
109 }
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400110 if cmdCount < 1 {
111 fmt.Println("CMD You didn't specify an operation to perform")
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400112 usage()
113 return
114 }
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400115
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400116 var conn *grpc.ClientConn
donNewtonAlphab3279ea2018-09-18 15:55:32 -0400117 var err error
118
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400119 // Setup the login/pass
120 auth := Authentication{
121 Login: "john",
122 Password: "doe",
123 }
donNewtonAlphab3279ea2018-09-18 15:55:32 -0400124 if *useSsl && *useAuth {
125
126 creds, err := credentials.NewClientTLSFromFile(*crtFile, *fqdn)
127 conn, err = grpc.Dial(*serverAddressPort, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth))
128 if err != nil {
129 log.Fatalf("could not load tls cert: %s", err)
130 }
131 } else if *useSsl {
132 creds, err := credentials.NewClientTLSFromFile("cert/server.crt", *fqdn)
133 conn, err = grpc.Dial(*serverAddressPort, grpc.WithTransportCredentials(creds))
134 if err != nil {
135 log.Fatalf("could not load tls cert: %s", err)
136 }
137 } else if *useAuth {
138 conn, err = grpc.Dial(*serverAddressPort, grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))
139 } else {
140 conn, err = grpc.Dial(*serverAddressPort, grpc.WithInsecure())
141 }
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400142 if err != nil {
143 log.Fatalf("did not connect: %s", err)
144 }
145 defer conn.Close()
146
147 c := api.NewAbstractOLTClient(conn)
148 if *create {
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400149 createChassis(c, clli, xosUser, xosPassword, xosAddress, xosPort, rack, shelf)
150 } else if *update {
151 updateXOSUserPassword(c, clli, xosUser, xosPassword)
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400152 } else if *addOlt {
153 addOltChassis(c, clli, oltAddress, oltPort, name, driver, oltType)
154 } else if *provOnt {
155 provisionONT(c, clli, slot, port, ont, serial)
Don Newton281e0252018-10-22 14:38:50 -0400156 } else if *provOntFull {
157 provisionONTFull(c, clli, slot, port, ont, serial, stag, ctag, nasPort, circuitID)
donNewtonAlphab3279ea2018-09-18 15:55:32 -0400158 } else if *echo {
159 ping(c, *message)
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400160 } else if *output {
161 Output(c)
162 } else if *deleteOnt {
163 deleteONT(c, clli, slot, port, ont, serial)
Don Newtone973d342018-10-26 16:44:12 -0400164 } else if *reflow {
165 reflowTosca(c)
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400166 }
167
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400168}
169
Author Namea594e632018-08-10 11:33:58 -0400170// Authentication holds the login/password
171type Authentication struct {
172 Login string
173 Password string
174}
175
176// GetRequestMetadata gets the current request metadata
177func (a *Authentication) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
178 return map[string]string{
179 "login": a.Login,
180 "password": a.Password,
181 }, nil
182}
183
184// RequireTransportSecurity indicates whether the credentials requires transport security
185func (a *Authentication) RequireTransportSecurity() bool {
186 return true
187}
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400188func Output(c api.AbstractOLTClient) error {
189 response, err := c.Output(context.Background(), &api.OutputMessage{Something: "wtf"})
190 if err != nil {
191 fmt.Printf("Error when calling Echo: %s", err)
192 return err
193 }
194 log.Printf("Response from server: %v", response.GetSuccess())
195
196 return nil
197}
198
donNewtonAlphab3279ea2018-09-18 15:55:32 -0400199func ping(c api.AbstractOLTClient, message string) error {
200 response, err := c.Echo(context.Background(), &api.EchoMessage{Ping: message})
201 if err != nil {
202 fmt.Printf("Error when calling Echo: %s", err)
203 return err
204 }
205 log.Printf("Response from server: %s", response.GetPong())
206 return nil
207}
Author Namea594e632018-08-10 11:33:58 -0400208
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400209func createChassis(c api.AbstractOLTClient, clli *string, xosUser *string, xosPassword *string, xosAddress *string, xosPort *uint, rack *uint, shelf *uint) error {
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400210 fmt.Println("Calling Create Chassis")
211 fmt.Println("clli", *clli)
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400212 fmt.Println("xos_user", *xosUser)
213 fmt.Println("xos_password", *xosPassword)
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400214 fmt.Println("xos_address", *xosAddress)
215 fmt.Println("xos_port", *xosPort)
216 fmt.Println("rack", *rack)
217 fmt.Println("shelf", *shelf)
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400218 response, err := c.CreateChassis(context.Background(), &api.AddChassisMessage{CLLI: *clli, XOSUser: *xosUser, XOSPassword: *xosPassword,
219 XOSIP: *xosAddress, XOSPort: int32(*xosPort), Rack: int32(*rack), Shelf: int32(*shelf)})
Author Namea594e632018-08-10 11:33:58 -0400220 if err != nil {
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400221 fmt.Printf("Error when calling CreateChassis: %s", err)
222 return err
Author Namea594e632018-08-10 11:33:58 -0400223 }
224 log.Printf("Response from server: %s", response.GetDeviceID())
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400225 return nil
226}
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400227func updateXOSUserPassword(c api.AbstractOLTClient, clli *string, xosUser *string, xosPassword *string) error {
228 fmt.Println("Calling Update XOS USER/PASSWORD")
229 fmt.Println("clli", *clli)
230 fmt.Println("xos_user", *xosUser)
231 fmt.Println("xos_password", *xosPassword)
232 response, err := c.ChangeXOSUserPassword(context.Background(), &api.ChangeXOSUserPasswordMessage{CLLI: *clli, XOSUser: *xosUser, XOSPassword: *xosPassword})
233 if err != nil {
234 fmt.Printf("Error when calling UpdateXOSUserPassword: %s", err)
235 return err
236 }
donNewtonAlphab1466392018-10-02 10:42:05 -0400237 log.Printf("Response from server: %t", response.GetSuccess())
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400238 return nil
239}
240
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400241func addOltChassis(c api.AbstractOLTClient, clli *string, oltAddress *string, oltPort *uint, name *string, driver *string, oltType *string) error {
242 fmt.Println("clli", *clli)
243 fmt.Println("olt_address", *oltAddress)
244 fmt.Println("olt_port", *oltPort)
245 fmt.Println("name", *name)
246 fmt.Println("driver", *driver)
247 fmt.Println("type", *oltType)
248 var driverType api.AddOLTChassisMessage_OltDriver
249 var chassisType api.AddOLTChassisMessage_OltType
250 switch *oltType {
251 case "edgecore":
252 chassisType = api.AddOLTChassisMessage_edgecore
253 }
254 switch *driver {
255 case "openolt":
256 driverType = api.AddOLTChassisMessage_openoltDriver
257
258 }
259
260 res, err := c.CreateOLTChassis(context.Background(), &api.AddOLTChassisMessage{CLLI: *clli, SlotIP: *oltAddress, SlotPort: uint32(*oltPort), Hostname: *name, Type: chassisType, Driver: driverType})
Author Namea594e632018-08-10 11:33:58 -0400261 if err != nil {
262 debug.PrintStack()
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400263 fmt.Printf("Error when calling CreateOLTChassis: %s", err)
264 return err
Author Namea594e632018-08-10 11:33:58 -0400265 }
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400266 log.Printf("Response from server: %s", res.GetDeviceID())
267 return nil
268}
269func provisionONT(c api.AbstractOLTClient, clli *string, slot *uint, port *uint, ont *uint, serial *string) error {
270 fmt.Println("clli", *clli)
271 fmt.Println("slot", *slot)
272 fmt.Println("port", *port)
273 fmt.Println("ont", *ont)
274 fmt.Println("serial", *serial)
275 res, err := c.ProvisionOnt(context.Background(), &api.AddOntMessage{CLLI: *clli, SlotNumber: int32(*slot), PortNumber: int32(*port), OntNumber: int32(*ont), SerialNumber: *serial})
276 if err != nil {
277 debug.PrintStack()
278 fmt.Printf("Error when calling ProvsionOnt %s", err)
279 return err
280 }
281 log.Printf("Response from server: %t", res.GetSuccess())
282 return nil
Don Newton281e0252018-10-22 14:38:50 -0400283}
284func provisionONTFull(c api.AbstractOLTClient, clli *string, slot *uint, port *uint, ont *uint, serial *string, stag *uint, ctag *uint, nasPort *string, circuitID *string) error {
285 fmt.Println("clli", *clli)
286 fmt.Println("slot", *slot)
287 fmt.Println("port", *port)
288 fmt.Println("ont", *ont)
289 fmt.Println("serial", *serial)
290 fmt.Println("stag", *stag)
291 fmt.Println("ctag", *ctag)
292 fmt.Println("nasPort", *nasPort)
293 fmt.Println("circuitID", *circuitID)
294 res, err := c.ProvisionOntFull(context.Background(), &api.AddOntFullMessage{CLLI: *clli, SlotNumber: int32(*slot), PortNumber: int32(*port), OntNumber: int32(*ont), SerialNumber: *serial, STag: uint32(*stag), CTag: uint32(*ctag), NasPortID: *nasPort, CircuitID: *circuitID})
295 if err != nil {
296 debug.PrintStack()
297 fmt.Printf("Error when calling ProvsionOnt %s", err)
298 return err
299 }
300 log.Printf("Response from server: %t", res.GetSuccess())
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400301 return nil
302}
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400303func deleteONT(c api.AbstractOLTClient, clli *string, slot *uint, port *uint, ont *uint, serial *string) error {
304 fmt.Println("clli", *clli)
305 fmt.Println("slot", *slot)
306 fmt.Println("port", *port)
307 fmt.Println("ont", *ont)
308 fmt.Println("serial", *serial)
309 res, err := c.DeleteOnt(context.Background(), &api.DeleteOntMessage{CLLI: *clli, SlotNumber: int32(*slot), PortNumber: int32(*port), OntNumber: int32(*ont), SerialNumber: *serial})
310 if err != nil {
311 debug.PrintStack()
312 fmt.Printf("Error when calling ProvsionOnt %s", err)
313 return err
314 }
315 log.Printf("Response from server: %t", res.GetSuccess())
316 return nil
317}
Don Newtone973d342018-10-26 16:44:12 -0400318func reflowTosca(c api.AbstractOLTClient) error {
319 res, err := c.Reflow(context.Background(), &api.ReflowMessage{})
320 if err != nil {
321 debug.PrintStack()
322 fmt.Printf("Error when calling Reflow %s", err)
323 return err
324 }
325 log.Printf("Response from server: %t", res.GetSuccess())
326 return nil
327}
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400328
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400329func usage() {
330 var output = `
donNewtonAlphab3279ea2018-09-18 15:55:32 -0400331 Usage ./client -server=[serverAddress:port] -[methodFlag] params
332 ./client -ssl -fqdn=FQDN_OF_ABSTRACT_OLT_SERVER.CRT -cert PATH_TO_SERVER.CRT -server=[serverAddress:port] -[methodFlag] params : use ssl
333 ./client -auth -server=[serverAddress:port] -[methodFlag] params : Authenticate session
334
335 methodFlags:
336 -e echo # used to test connectivity to server NOOP
337 params:
338 -message string to be echoed back from the server
339 e.g. ./client -server=localhost:7777 -e -message MESSAGE_TO_BE_ECHOED
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400340 -c create chassis
341 params:
342 -clli CLLI_NAME
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400343 -xos_user XOS_USER
344 -xos_password XOS_PASSWORD
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400345 -xos_address XOS_TOSCA_IP
346 -xos_port XOS_TOSCA_LISTEN_PORT
347 -rack [optional default 1]
348 -shelf [optional default 1]
donNewtonAlpha57aa2ff2018-10-01 16:45:32 -0400349 e.g. ./client -server=localhost:7777 -c -clli MY_CLLI -xos_user foundry -xos_password password -xos_address 192.168.0.1 -xos_port 30007 -rack 1 -shelf 1
350 -u update xos user/password
351 -clli CLLI_NAME
352 -xos_user XOS_USER
353 -xos_password XOS_PASSWORD
354 e.g. ./client -server=localhost:7777 -u -clli MY_CLLI -xos_user NEW_USER -xos_password NEW_PASSWORD
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400355 -s add physical olt chassis to chassis
356 params:
357 -clli CLLI_NAME - identifies abstract chassis to assign olt chassis to
358 -olt_address - OLT_CHASSIS_IP_ADDRESS
359 -olt_port - OLT_CHASSIS_LISTEN_PORT
360 -name - OLT_NAME internal human readable name to identify OLT_CHASSIS
361 -driver [openolt,asfvolt16,adtran,tibits] - used to tell XOS which driver should be used to manange chassis
362 -type [edgecore,adtran,tibit] - used to tell AbstractOLT how many ports are available on olt chassis
donNewtonAlphab3279ea2018-09-18 15:55:32 -0400363 e.g. ./client -server abstractOltHost:7777 -s -clli MY_CLLI -olt_address 192.168.1.100 -olt_port=9191 -name=slot1 -driver=openolt -type=adtran
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400364 -o provision ont - adds ont to whitelist in XOS on a specific port on a specific olt chassis based on abstract -> phyisical mapping
365 params:
366 -clli CLLI_NAME
367 -slot SLOT_NUMBER [1-16]
368 -port OLT_PORT_NUMBER [1-16]
369 -ont ONT_NUMBER [1-64]
370 -serial ONT_SERIAL_NUM
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400371 e.g. ./client -server=localhost:7777 -o -clli=MY_CLLI -slot=1 -port=1 -ont=22 -serial=aer900jasdf
Don Newton281e0252018-10-22 14:38:50 -0400372 -f provision ont full - same as -o above but allows explicit set of s/c vlans , NasPortID and CircuitID
373 params:
374 -clli CLLI_NAME
375 -slot SLOT_NUMBER [1-16]
376 -port OLT_PORT_NUMBER [1-16]
377 -ont ONT_NUMBER [1-64]
378 -serial ONT_SERIAL_NUM
379 -stag S_TAG
380 -ctag C_TAG
381 -nas_port NAS_PORT_ID
382 -circuit_id CIRCUIT_ID
383 e.g. ./client -server=localhost:7777 -f -clli=MY_CLLI -slot=1 -port=1 -ont=22 -serial=aer900jasdf -stag=33 -ctag=104 -nas_port="pon 1/1/1/3:1.1" -circuit_id="CLLI 1/1/1/13:1.1"
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400384 -d delete ont - removes ont from service
385 params:
386 -clli CLLI_NAME
387 -slot SLOT_NUMBER [1-16]
388 -port OLT_PORT_NUMBER [1-16]
389 -ont ONT_NUMBER [1-64]
390 -serial ONT_SERIAL_NUM
391 e.g. ./client -server=localhost:7777 -d -clli=MY_CLLI -slot=1 -port=1 -ont=22 -serial=aer900jasdf
392 -output (TEMPORARY) causes AbstractOLT to serialize all chassis to JSON file in $WorkingDirectory/backups
393 e.g. ./client -server=localhost:7777 -output
Don Newtone973d342018-10-26 16:44:12 -0400394 -reflow causes tosca to be repushed to xos
395 e.g. ./client -server=localhost:7777 -reflow
donNewtonAlphae7ab5b92018-09-27 15:09:14 -0400396 `
donNewtonAlpha1d2d6812018-09-14 16:00:02 -0400397
398 fmt.Println(output)
Author Namea594e632018-08-10 11:33:58 -0400399}