02-12-2014 11:30 AM
I am trying to write some Asynchronous TCL code to make things more efficent. This is just a simple example, but in reality it will SSH into a device, run a command, do some math, and write the output to the switch.
The whole "after" thing is throwing me off. When i run it locally trough tclsh86 , i need to run the "update" command to follow up with the results. How and what do i need to do to get this to work?
#***************************************CODE**********************************************************
::cisco::eem::event_register_timer watchdog time 15 maxrun 15
namespace import ::cisco::eem::*
namespace import ::cisco::lib::*
proc checkDevice { interface tempName aNumber waitTime} {
puts " "
puts "$tempName - Start"
after $waitTime
puts "$tempName - After"
if [catch {cli_write $interface "event manager environment tempName $aNumber"} _cli_result] {
error $_cli_result $errorInfo
}
puts "$tempName - EXIT"
}
if [catch {cli_open} result] {
error $result $errorInfo
} else {
array set cli1 $result
}
if [catch {cli_exec $cli1(fd) "enable"} _cli_result] {
error $_cli_result $errorInfo
}
after 0 checkDevice $cli1(fd) A 11 1000
after 0 checkDevice $cli1(fd) B 22 200
after 0 checkDevice $cli1(fd) C 33 4000
after 0 checkDevice $cli1(fd) D 44 1000
if [catch {cli_exec $cli1(fd) "exit"} _cli_result] {
error $_cli_result $errorInfo
}
catch {cli_close $cli1(fd) $cli1(tty_id)} result
02-12-2014 01:03 PM
It would likely help to understand your final goal. This example has many things that aren't correct, so understanding your desired end goal would be useful to make sure EEM will actually do what you want.
02-12-2014 01:29 PM
I have multiple devices (1 - 12) i need to SSH into to pull data from. When i get that data, I need to compare it with my current configuration on the switch and make adjustments accordingly.
Basically building off of this https://supportforums.cisco.com/thread/2249735
Before I had a script (DeviceReader1.tcl) that opened a vty to the switch, read in the 30 second average from the switch, then SSHed into the device , took a sample of the data 1 time a second for 5 seconds, then it would exit out of the device putting it back into the switch, I then injected my new data into the 30 second average, and rewrote the data back to the event manager eviroment variable. I then had a seperate script (Adjuster.tcl) that constantly read the global variables and calculated the data and made any changes to the switch if it needed it.
I realized that in my (DeviceReader) script that i could open 2 connections, 1 for the ssh, and other just for the switch. That way, instead of having to re-SSH into the device, I could keep it open, and still be able to write to the environment variable. And the Adjuster script works like before.
The issue that I will run into is the limited number of VTY channels available. This will work when we have only 6 or so devices configured, but anything more this wont work.
I then realized its possible to do Asyncronous Calls with TCL. So, instead of having the X DeviceReader scripts and Adjuster script, i could have 1 file that could take care of everything almost doubling the amount of devices i could use, and probably consume a lot less resources as well as more efficent use of variables.
So, i guess a couple of other questions i have is, when you have 2 seperate scripts running, whats the best way to share variables? Also, how do i setup a script to start on startup and run indefinetly?
Note: I just found EEM_HTTP_SERVER_CODE example and understanding how some of that works.
02-12-2014 03:02 PM
Ah, okay. What you want to do should be possible. For async programming, the trick is to use vwait to block while your asynchronous things run. For example:
set exiting 0
proc check_q { } {
global poll_timer poll_q exiting
if { $poll_q != "" } {
puts "Q : $poll_q"
if { $poll_q == "DIE" } {
set exiting 1
}
set poll_q {}
}
after $poll_timer {::check_q}
}
after $poll_timer {::check_q}
vwait exiting
The vwait call blocks until the variable on which it's waiting is true. So in this case, it will run until the queue somehow contains the word, "DIE." Does that help?
02-12-2014 04:08 PM
When i run this, it only loops A. Can i not have multiple process going at once? How can I send a commands to different devices in parallell? Do each of the devices need its own proc?
Note: Some devices can be off or slower, so i prefer to not trigger each one once in a loop, then wait for all the replies, then loop again.
*****
Apr 8 09:37:30.305: %HA_EM-6-LOG: SSHTest2.tcl: Start
Apr 8 09:37:30.646: %HA_EM-6-LOG: SSHTest2.tcl: Before
Apr 8 09:37:30.646: %HA_EM-6-LOG: SSHTest2.tcl: After
Apr 8 09:37:30.646: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Apr 8 09:37:30.646: %HA_EM-6-LOG: SSHTest2.tcl: A - Start
Apr 8 09:37:31.647: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 8 09:37:32.649: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 8 09:37:33.650: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 8 09:37:34.651: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 8 09:37:35.668: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 8 09:37:36.686: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
...
*******
::cisco::eem::event_register_timer watchdog time 30 maxrun 30
namespace import ::cisco::eem::*
namespace import ::cisco::lib::*
set exiting 0
set poll_timer 100
set poll_q {}
proc checkDevice { interface tempName aNumber waitTime} {
puts "$tempName - Start"
while {1} {
global poll_q
after $waitTime
puts "$tempName - Fired"
if [catch {cli_write $interface "event manager environment tempName $aNumber"} _cli_result] {
error $_cli_result $errorInfo
}
set poll_q "$tempName + $aNumber"
}
}
proc check_q { } {
global poll_timer poll_q exiting
if { $poll_q != "" } {
puts "Q : $poll_q"
if { $poll_q == "DIE" } {
set exiting 1
}
set poll_q {}
}
after $poll_timer {::check_q}
}
puts "Start"
if [catch {cli_open} result] {
error $result $errorInfo
} else {
array set cli1 $result
}
if [catch {cli_exec $cli1(fd) "enable"} _cli_result] {
error $_cli_result $errorInfo
}
puts "Before"
after 0 checkDevice $cli1(fd) A 11 1000
after 0 checkDevice $cli1(fd) B 22 200
after 0 checkDevice $cli1(fd) C 33 4000
after 0 checkDevice $cli1(fd) D 44 1000
puts "After"
after $poll_timer {::check_q}
puts "After Timer"
vwait exiting
puts "After vwait"
if [catch {cli_exec $cli1(fd) "exit"} _cli_result] {
error $_cli_result $errorInfo
}
catch {cli_close $cli1(fd) $cli1(tty_id)} result
02-12-2014 04:18 PM
You're entering an infinite loop in your callback. You don't want to do this. If you need to continually call into it, have it set an after: For example:
proc checkDevice { interface tempName aNumber waitTime} {
puts "$tempName - Start"
after $waitTime {procDevice $interface $tempName $aNumber $waitTime}
}
proc procDevice { interface tempName aNumber waitTime } {
puts "$tempName - Fired"
if [catch {cli_write $interface "event manager environment $tempName $aNumber"} _cli_result] {
error $_cli_result $errorInfo
}
set poll_q "$tempName + $aNumber"
after $waitTime {procDevice $interface $tempName $aNumber $waitTime}
}
02-12-2014 04:45 PM
So i added the global poll_q to the procDevice because it may need to fire an event. I also realized i forgot to put a $ in front of tempName in the cli_write command. I also had forgotten to have set "conf t" before writing the event manager enviroment variables.
With or without those changes, while it starts those processes, it never steps into ProcDevice. Another thing that was odd was the the CLI was running very sluggish. Entering "show run | sec event" took a second or so to respond. Also, there was no variables set either not even a tempName.
Apr 8 10:09:59.372: %HA_EM-6-LOG: SSHTest2.tcl: Start
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: Before
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: After
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: A - Start
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: B - Start
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: C - Start
Apr 8 10:10:00.069: %HA_EM-6-LOG: SSHTest2.tcl: D - Start
Apr 8 10:10:17.769: %HA_EM-6-LOG: SSHTest2.tcl: After vwait
Apr 8 10:10:17.769: %HA_EM-6-LOG: SSHTest2.tcl: Process Forced Exit- MAXRUN timer expired.
::cisco::eem::event_register_timer watchdog time 20 maxrun 20
namespace import ::cisco::eem::*
namespace import ::cisco::lib::*
set exiting 0
set poll_timer 100
set poll_q {}
proc checkDevice { interface tempName aNumber waitTime} {
puts "$tempName - Start"
after $waitTime {procDevice $interface $tempName $aNumber $waitTime}
}
proc procDevice { interface tempName aNumber waitTime } {
global poll_q
puts "$tempName - Fired"
if [catch {cli_write $interface "event manager environment $tempName $aNumber"} _cli_result] {
error $_cli_result $errorInfo
}
set poll_q "$tempName + $aNumber"
after $waitTime {procDevice $interface $tempName $aNumber $waitTime}
}
proc check_q { } {
global poll_timer poll_q exiting
if { $poll_q != "" } {
puts "Q : $poll_q"
if { $poll_q == "DIE" } {
set exiting 1
}
set poll_q {}
}
after $poll_timer {::check_q}
}
puts "Start"
if [catch {cli_open} result] {
error $result $errorInfo
} else {
array set cli1 $result
}
if [catch {cli_exec $cli1(fd) "enable"} _cli_result] {
error $_cli_result $errorInfo
}
if [catch {cli_exec $cli1(fd) "config t"} _cli_result] {
error $_cli_result $errorInfo
}
puts "Before"
after 0 checkDevice $cli1(fd) A 11 1000
after 0 checkDevice $cli1(fd) B 22 200
after 0 checkDevice $cli1(fd) C 33 4000
after 0 checkDevice $cli1(fd) D 44 1000
puts "After"
after $poll_timer {::check_q}
puts "After Timer"
vwait exiting
puts "After vwait"
if [catch {cli_exec $cli1(fd) "exit"} _cli_result] {
error $_cli_result $errorInfo
}
catch {cli_close $cli1(fd) $cli1(tty_id)} result
02-12-2014 05:06 PM
Remove the {} around the calls to procDevice:
after $waitTime procDevice $interface $tempName $aNumber $waitTime
And you'll want to increase your maxrun to something like 120 to let it run for a while.
02-12-2014 06:26 PM
Its firing once... but its not looping properly. It also causes the switch to run very slow.
Apr 8 11:20:34.098: %HA_EM-6-LOG: SSHTest2.tcl: Start
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: Before
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: After
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: A - Start
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: B - Start
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: C - Start
Apr 8 11:20:34.796: %HA_EM-6-LOG: SSHTest2.tcl: D - Start
Apr 8 11:20:35.005: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 8 11:20:35.095: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 8 11:20:35.797: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 8 11:20:35.808: %HA_EM-6-LOG: SSHTest2.tcl: D - Fired
Apr 8 11:20:35.808: %HA_EM-6-LOG: SSHTest2.tcl: Q : D + 44
Apr 8 11:20:38.796: %HA_EM-6-LOG: SSHTest2.tcl: C - Fired
Apr 8 11:20:38.828: %HA_EM-6-LOG: SSHTest2.tcl: Q : C + 33
Apr 8 11:22:32.907: %HA_EM-6-LOG: SSHTest2.tcl: After vwait
Apr 8 11:22:32.913: %HA_EM-6-LOG: SSHTest2.tcl: Process Forced Exit- MAXRUN timer expired.
02-12-2014 08:23 PM
What does your current script look like? Also, you may want to increase your poll timers to be at least 1000. The tighter the loop, the hotter the device will run.
02-13-2014 09:12 AM
I left it at 2 minutes because it shouldnt take a minute or so to warm up, and the rate that it iterates should be within 5 seconds max. I was also wondering why i am starting up Proc checkDevice which runs procDevice, which then reruns itself. wouldn't it just make more sence to initally target procDevice to begin with?
::cisco::eem::event_register_timer watchdog time 120 maxrun 120
namespace import ::cisco::eem::*
namespace import ::cisco::lib::*
set exiting 0
set poll_timer 100
set poll_q {}
proc checkDevice { interface tempName aNumber waitTime} {
puts "$tempName - Start"
after $waitTime procDevice $interface $tempName $aNumber $waitTime
}
proc procDevice { interface tempName aNumber waitTime } {
global poll_q
puts "$tempName - Fired"
if [catch {cli_write $interface "event manager environment $tempName $aNumber"} _cli_result] {
error $_cli_result $errorInfo
}
set poll_q "$tempName + $aNumber"
after $waitTime {procDevice $interface $tempName $aNumber $waitTime}
}
proc check_q { } {
global poll_timer poll_q exiting
if { $poll_q != "" } {
puts "Q : $poll_q"
if { $poll_q == "DIE" } {
set exiting 1
}
set poll_q {}
}
after $poll_timer {::check_q}
}
puts "Start"
if [catch {cli_open} result] {
error $result $errorInfo
} else {
array set cli1 $result
}
if [catch {cli_exec $cli1(fd) "enable"} _cli_result] {
error $_cli_result $errorInfo
}
if [catch {cli_exec $cli1(fd) "config t"} _cli_result] {
error $_cli_result $errorInfo
}
puts "Before"
after 0 checkDevice $cli1(fd) A 11 1000
after 0 checkDevice $cli1(fd) B 22 200
after 0 checkDevice $cli1(fd) C 33 4000
after 0 checkDevice $cli1(fd) D 44 1000
puts "After"
after $poll_timer {::check_q}
puts "After Timer"
vwait exiting
puts "After vwait"
if [catch {cli_exec $cli1(fd) "exit"} _cli_result] {
error $_cli_result $errorInfo
}
catch {cli_close $cli1(fd) $cli1(tty_id)} result
02-13-2014 09:38 AM
After swapping the checkDevice calls "after 0 checkDevice $cli1(fd) A 11 1000" with "after 0 procDevice $cli1(fd) A 11 1000" , it only fires D once and it hangs after. I reaaaaaaaly dont understand why.
Apr 9 03:00:37.085: %HA_EM-6-LOG: SSHTest2.tcl: Start
Apr 9 03:00:37.651: %HA_EM-6-LOG: SSHTest2.tcl: Before
Apr 9 03:00:37.651: %HA_EM-6-LOG: SSHTest2.tcl: After
Apr 9 03:00:37.651: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Apr 9 03:00:37.651: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 9 03:00:37.651: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:00:37.651: %HA_EM-6-LOG: SSHTest2.tcl: C - Fired
Apr 9 03:00:37.657: %HA_EM-6-LOG: SSHTest2.tcl: D - Fired
Apr 9 03:00:37.751: %HA_EM-6-LOG: SSHTest2.tcl: Q : D + 44
Apr 9 03:02:36.182: %HA_EM-6-LOG: SSHTest2.tcl: After vwait
Apr 9 03:02:36.182: %HA_EM-6-LOG: SSHTest2.tcl: Process Forced Exit- MAXRUN timer expired.
I then removed the { } in "after $waitTime {procDevice $interface $tempName $aNumber $waitTime}" and wholy crap.... i think it works....
Apr 9 03:09:47.331: %HA_EM-6-LOG: SSHTest2.tcl: Start
Apr 9 03:09:47.876: %HA_EM-6-LOG: SSHTest2.tcl: Before
Apr 9 03:09:47.876: %HA_EM-6-LOG: SSHTest2.tcl: After
Apr 9 03:09:47.876: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Apr 9 03:09:47.876: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 9 03:09:47.876: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:47.876: %HA_EM-6-LOG: SSHTest2.tcl: C - Fired
Apr 9 03:09:47.881: %HA_EM-6-LOG: SSHTest2.tcl: D - Fired
Apr 9 03:09:47.975: %HA_EM-6-LOG: SSHTest2.tcl: Q : D + 44
Apr 9 03:09:48.080: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:48.091: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:48.290: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:48.306: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:48.505: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:48.515: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:48.715: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:48.730: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:48.877: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 9 03:09:48.882: %HA_EM-6-LOG: SSHTest2.tcl: D - Fired
Apr 9 03:09:48.930: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:48.945: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:49.145: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:49.150: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:49.349: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:49.370: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:49.569: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:49.580: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:49.779: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:49.789: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:49.879: %HA_EM-6-LOG: SSHTest2.tcl: A - Fired
Apr 9 03:09:49.889: %HA_EM-6-LOG: SSHTest2.tcl: D - Fired
Apr 9 03:09:49.894: %HA_EM-6-LOG: SSHTest2.tcl: Q : D + 44
Apr 9 03:09:49.989: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
Apr 9 03:09:49.999: %HA_EM-6-LOG: SSHTest2.tcl: Q : B + 22
Apr 9 03:09:50.198: %HA_EM-6-LOG: SSHTest2.tcl: B - Fired
02-13-2014 09:43 AM
Increased the poll rate to 10ms from 100, I removed the X- Fired puts, changed the message to be easier to see....
Apr 9 03:15:40.171: %HA_EM-6-LOG: SSHTest2.tcl: Start
Apr 9 03:15:40.737: %HA_EM-6-LOG: SSHTest2.tcl: Before
Apr 9 03:15:40.737: %HA_EM-6-LOG: SSHTest2.tcl: After
Apr 9 03:15:40.737: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Apr 9 03:15:40.737: %HA_EM-6-LOG: SSHTest2.tcl: A A A
Apr 9 03:15:40.737: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:40.743: %HA_EM-6-LOG: SSHTest2.tcl: C C C
Apr 9 03:15:40.743: %HA_EM-6-LOG: SSHTest2.tcl: D D D
Apr 9 03:15:40.942: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:41.141: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:41.340: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:41.545: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:41.739: %HA_EM-6-LOG: SSHTest2.tcl: A A A
Apr 9 03:15:41.744: %HA_EM-6-LOG: SSHTest2.tcl: D D D
Apr 9 03:15:41.744: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:41.943: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:42.148: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:42.357: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Apr 9 03:15:42.562: %HA_EM-6-LOG: SSHTest2.tcl: B B B
Its getting better..... I realized i can remove the after poll_timer
and everything looks right... but CPU is out the roof. It feels like recursion isnt the right approach.
3560Switch#show process cpu | inc TCL
409 77963 502 155304 85.97% 65.55% 21.55% 0 EEM TCL Proc
02-13-2014 09:46 AM
You could call procDevice directly. The only reason I broke it out is because you had that extra syslog message in there. But regardless you need to change this line in procDevice:
after $waitTime {procDevice $interface $tempName $aNumber $waitTime}
To:
after $waitTime procDevice $interface $tempName $aNumber $waitTime
That will fix the misfiring issue and may help CPU.
02-13-2014 11:04 AM
I had done that with no luck. CPU still burning. I am wondering if its the recursion or what...
I tried to put it in an indefinate loop, but that doesnt work. I used the ping so it would cause a delay without having to use "after" It never steps into the while nor does it pass it.
proc procDevice { interface tempName aNumber waitTime } {
puts "$tempName"
while{1} {
global poll_q
puts "$tempName $tempName $tempName"
if [catch {cli_exec $interface "#ping 1.1.4.1 size 15000 repeat $waitTime"} _cli_result] {
error $_cli_result $errorInfo
}
}
puts "This shouldnt appear"
}
Mar 30 01:53:02.383: %HA_EM-6-LOG: SSHTest2.tcl: Start
Mar 30 01:53:02.823: %HA_EM-6-LOG: SSHTest2.tcl: Before
Mar 30 01:53:02.823: %HA_EM-6-LOG: SSHTest2.tcl: After
Mar 30 01:53:02.823: %HA_EM-6-LOG: SSHTest2.tcl: After Timer
Mar 30 01:53:02.823: %HA_EM-6-LOG: SSHTest2.tcl: A
Mar 30 01:53:02.828: %HA_EM-6-LOG: SSHTest2.tcl: B
Mar 30 01:53:02.833: %HA_EM-6-LOG: SSHTest2.tcl: C
Mar 30 01:53:02.839: %HA_EM-6-LOG: SSHTest2.tcl: D
Mar 30 01:53:21.236: %HA_EM-6-LOG: SSHTest2.tcl: After vwait
Mar 30 01:53:21.236: %HA_EM-6-LOG: SSHTest2.tcl: Process Forced Exit- MAXRUN timer expired.
Discover and save your favorite ideas. Come back to expert answers, step-by-step guides, recent topics, and more.
New here? Get started with these tips. How to use Community New member guide