├── LICENSE ├── README.md ├── format.go ├── main.go ├── sshconfig.go ├── sshhelper.go └── stats.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 RapidLoop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # rtop-bot 3 | 4 | [![Join the chat at https://gitter.im/rapidloop/rtop-bot](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rapidloop/rtop-bot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | *rtop-bot* is a bot front-end to *rtop*. 7 | 8 | *rtop* can connect over SSH to Linux systems and display their vital system 9 | metrics without needing any agent on the target system. *rtop-bot* can do 10 | this when asked to, over HipChat or Slack. *rtop-bot* is independent of *rtop*. 11 | 12 | *rtop-bot* is self-hosted. You can run it on a machine within your 13 | secure network from which it can SSH to target systems. When run, it will 14 | connect to Slack as a bot (or to a HipChat room as the user you specify), and 15 | listen for mentions: 16 | 17 | you | @rtop-bot status some.host 18 | rtop-bot| [some.host] up 34d 20h 1m 51s, load 0.08 0.03 0.05, procs 1 running of 131 total 19 | rtop-bot| [some.host] mem: 45.55 MiB of 489.57 MiB free, swap 0 bytes of 0 bytes free 20 | rtop-bot| [some.host] fs /: 16.18 GiB of 18.55 GiB free 21 | 22 | *rtop-bot*'s [home page](http://www.rtop-monitor.org/rtop-bot) has more 23 | information and screenshots! 24 | 25 | ## build 26 | 27 | *rtop-bot* is written in [go](http://golang.org/), and requires Go version 1.2 28 | or higher. To build, *go get* it: 29 | 30 | go get github.com/rapidloop/rtop-bot 31 | 32 | You should find the binary *rtop-bot* under *$GOPATH/bin* when the command 33 | completes. There are no runtime dependencies or configuration needed. 34 | 35 | ## contribute 36 | 37 | Pull requests welcome. Keep it simple. 38 | 39 | ## changelog 40 | * 4-Sep-2015: 0.2 - Slack support added 41 | * 11-Aug-2015: 0.1 - first public release 42 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | rtop-bot - remote system monitoring bot 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | ) 32 | 33 | func fmtUptime(stats *Stats) string { 34 | dur := stats.Uptime 35 | dur = dur - (dur % time.Second) 36 | var days int 37 | for dur.Hours() > 24.0 { 38 | days++ 39 | dur -= 24 * time.Hour 40 | } 41 | s1 := dur.String() 42 | s2 := "" 43 | if days > 0 { 44 | s2 = fmt.Sprintf("%dd ", days) 45 | } 46 | for _, ch := range s1 { 47 | s2 += string(ch) 48 | if ch == 'h' || ch == 'm' { 49 | s2 += " " 50 | } 51 | } 52 | return s2 53 | } 54 | 55 | func fmtBytes(val uint64) string { 56 | if val < 1024 { 57 | return fmt.Sprintf("%d bytes", val) 58 | } else if val < 1024*1024 { 59 | return fmt.Sprintf("%.2f KiB", float64(val)/1024.0) 60 | } else if val < 1024*1024*1024 { 61 | return fmt.Sprintf("%.2f MiB", float64(val)/1024.0/1024.0) 62 | } else { 63 | return fmt.Sprintf("%.2f GiB", float64(val)/1024.0/1024.0/1024.0) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | rtop-bot - remote system monitoring bot 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "flag" 30 | "fmt" 31 | "log" 32 | "os" 33 | "os/user" 34 | "path/filepath" 35 | "strings" 36 | "time" 37 | 38 | "github.com/daneharrigan/hipchat" 39 | "github.com/nlopes/slack" 40 | ) 41 | 42 | const ( 43 | VERSION = "0.2" 44 | ) 45 | 46 | var sshUsername, idRsaPath string 47 | var hcFlag = flag.Bool("h", false, "create HipChat bot") 48 | var slackFlag = flag.Bool("s", false, "create Slack bot") 49 | var currentUser *user.User 50 | 51 | //---------------------------------------------------------------------------- 52 | 53 | func usage() { 54 | fmt.Printf( 55 | `rtop-bot %s - (c) 2015 RapidLoop - http://www.rtop-monitor.org/rtop-bot 56 | rtop-bot is a Slack and HipChat bot that can do remote system monitoring over SSH 57 | 58 | Usage: 59 | rtop-bot -s slackBotToken 60 | rtop-bot -h hipChatUserJid hipChatRoomJid 61 | 62 | where: 63 | slackBotToken is the API token for the Slack bot 64 | hipChatuserJid is the HipChat user jabber ID, like 139999_999914 65 | hipChatRoomJid is the HipChat room jabber ID, like 139999_opschat 66 | `, VERSION) 67 | os.Exit(1) 68 | } 69 | 70 | func main() { 71 | 72 | flag.Parse() 73 | 74 | if (!*hcFlag && !*slackFlag) || (*hcFlag && *slackFlag) || 75 | (*hcFlag && len(os.Args) != 4) || (*slackFlag && len(os.Args) != 3) { 76 | usage() 77 | } 78 | 79 | log.SetPrefix("rtop-bot: ") 80 | log.SetFlags(0) 81 | 82 | // get default username for SSH connections 83 | var err error 84 | if currentUser, err = user.Current(); err != nil { 85 | log.Print(err) 86 | os.Exit(1) 87 | } 88 | sshUsername = currentUser.Username 89 | 90 | // expand ~/.ssh/id_rsa and check if it exists 91 | idRsaPath = filepath.Join(currentUser.HomeDir, ".ssh", "id_rsa") 92 | if _, err := os.Stat(idRsaPath); os.IsNotExist(err) { 93 | idRsaPath = "" 94 | } 95 | 96 | // expand ~/.ssh/config and parse if it exists 97 | sshConfig := filepath.Join(currentUser.HomeDir, ".ssh", "config") 98 | if _, err := os.Stat(sshConfig); err == nil { 99 | parseSshConfig(sshConfig) 100 | } 101 | 102 | if *hcFlag { 103 | doHipChat(os.Args[2], os.Args[3]) 104 | } else { 105 | doSlack(os.Args[2]) 106 | } 107 | } 108 | 109 | func doSlack(apiToken string) { 110 | api := slack.New(apiToken) 111 | rtm := api.NewRTM() 112 | go rtm.ManageConnection() 113 | 114 | mention := "" 115 | for msg := range rtm.IncomingEvents { 116 | switch ev := msg.Data.(type) { 117 | case *slack.ConnectedEvent: 118 | mention = "<@" + ev.Info.User.ID + ">" 119 | if ev.ConnectionCount == 1 { 120 | log.Printf("bot [%s] ready", ev.Info.User.Name) 121 | log.Print("hit ^C to exit") 122 | } else { 123 | log.Printf("bot [%s] reconnected", ev.Info.User.Name) 124 | } 125 | case *slack.MessageEvent: 126 | if strings.HasPrefix(ev.Msg.Text, mention) { 127 | t := strings.TrimPrefix(ev.Msg.Text, mention) 128 | go func(text, ch string) { 129 | r := process(text) 130 | rtm.SendMessage(rtm.NewOutgoingMessage(r, ch)) 131 | }(t, ev.Msg.Channel) 132 | } 133 | case *slack.InvalidAuthEvent: 134 | log.Print("bad Slack API token") 135 | os.Exit(1) 136 | } 137 | } 138 | } 139 | 140 | func doHipChat(username, roomjid string) { 141 | if strings.HasSuffix(username, "@chat.hipchat.com") { 142 | username = strings.Replace(username, "@chat.hipchat.com", "", 1) 143 | } 144 | if !strings.HasSuffix(roomjid, "@conf.hipchat.com") { 145 | roomjid += "@conf.hipchat.com" 146 | } 147 | pass, err := getpass("Password for user \"" + username + "\": ") 148 | if err != nil { 149 | log.Print(err) 150 | } 151 | 152 | client, err := hipchat.NewClient(username, pass, "bot") 153 | if err != nil { 154 | log.Print(err) 155 | os.Exit(1) 156 | } 157 | 158 | nick, mname := getUserInfo(client, username) 159 | 160 | client.Status("chat") 161 | client.Join(roomjid, nick) 162 | log.Printf("[%s] now serving room [%s]", nick, roomjid) 163 | log.Print("hit ^C to exit") 164 | 165 | go client.KeepAlive() 166 | for message := range client.Messages() { 167 | if strings.HasPrefix(message.Body, "@"+mname) { 168 | go client.Say(roomjid, nick, process(message.Body)) 169 | } 170 | } 171 | } 172 | 173 | func getUserInfo(client *hipchat.Client, id string) (string, string) { 174 | id = id + "@chat.hipchat.com" 175 | client.RequestUsers() 176 | select { 177 | case users := <-client.Users(): 178 | for _, user := range users { 179 | if user.Id == id { 180 | log.Printf("using username [%s] and mention name [%s]", 181 | user.Name, user.MentionName) 182 | return user.Name, user.MentionName 183 | } 184 | } 185 | case <-time.After(10 * time.Second): 186 | log.Print("timed out waiting for user list") 187 | os.Exit(1) 188 | } 189 | return "rtop-bot", "rtop-bot" 190 | } 191 | 192 | func process(request string) string { 193 | 194 | parts := strings.Fields(request) 195 | if len(parts) != 3 || parts[1] != "status" { 196 | return "say \"status \" to see vital stats of " 197 | } 198 | 199 | address, user, keypath := getSshEntryOrDefault(parts[2]) 200 | client, err := sshConnect(user, address, keypath) 201 | if err != nil { 202 | return fmt.Sprintf("[%s]: %v", parts[2], err) 203 | } 204 | 205 | stats := Stats{} 206 | getAllStats(client, &stats) 207 | result := fmt.Sprintf( 208 | `[%s] up %s, load %s %s %s, procs %s running of %s total 209 | [%s] mem: %s of %s free, swap %s of %s free 210 | `, 211 | stats.Hostname, fmtUptime(&stats), stats.Load1, stats.Load5, 212 | stats.Load10, stats.RunningProcs, stats.TotalProcs, 213 | stats.Hostname, fmtBytes(stats.MemFree), fmtBytes(stats.MemTotal), 214 | fmtBytes(stats.SwapFree), fmtBytes(stats.SwapTotal), 215 | ) 216 | if len(stats.FSInfos) > 0 { 217 | for _, fs := range stats.FSInfos { 218 | result += fmt.Sprintf("[%s] fs %s: %s of %s free\n", 219 | stats.Hostname, 220 | fs.MountPoint, 221 | fmtBytes(fs.Free), 222 | fmtBytes(fs.Used+fs.Free), 223 | ) 224 | } 225 | } 226 | return result 227 | } 228 | -------------------------------------------------------------------------------- /sshconfig.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | rtop-bot - remote system monitoring bot 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "bufio" 30 | "fmt" 31 | "log" 32 | "os" 33 | "path" 34 | "strconv" 35 | "strings" 36 | ) 37 | 38 | type Section struct { 39 | Hostname string 40 | Port int 41 | User string 42 | IdentityFile string 43 | } 44 | 45 | func (s *Section) clear() { 46 | s.Hostname = "" 47 | s.Port = 0 48 | s.User = "" 49 | s.IdentityFile = "" 50 | } 51 | 52 | func (s *Section) getFull(name string, def Section) (address, user, keyfile string) { 53 | h := name 54 | if len(s.Hostname) > 0 { 55 | h = s.Hostname 56 | } else if len(def.Hostname) > 0 { 57 | h = def.Hostname 58 | } 59 | p := 22 60 | if s.Port > 0 { 61 | p = s.Port 62 | } else if def.Port > 0 { 63 | p = def.Port 64 | } 65 | address = fmt.Sprintf("%s:%d", h, p) 66 | if len(s.User) > 0 { 67 | user = s.User 68 | } else if len(def.User) > 0 { 69 | user = def.User 70 | } else { 71 | user = sshUsername 72 | } 73 | if len(s.IdentityFile) > 0 { 74 | keyfile = s.IdentityFile 75 | } else if len(def.IdentityFile) > 0 { 76 | keyfile = def.IdentityFile 77 | } else { 78 | keyfile = idRsaPath 79 | } 80 | return 81 | } 82 | 83 | var HostInfo = make(map[string]Section) 84 | 85 | func getSshEntryOrDefault(name string) (address, user, keyfile string) { 86 | 87 | def := Section{Hostname: name} 88 | if defcfg, ok := HostInfo["*"]; ok { 89 | def = defcfg 90 | } 91 | 92 | if s, ok := HostInfo[name]; ok { 93 | return s.getFull(name, def) 94 | } 95 | for h, s := range HostInfo { 96 | if ok, err := path.Match(h, name); ok && err == nil { 97 | return s.getFull(name, def) 98 | } 99 | } 100 | return def.getFull(name, def) 101 | } 102 | 103 | func parseSshConfig(path string) { 104 | f, err := os.Open(path) 105 | if err != nil { 106 | log.Printf("warning: %v", err) 107 | return 108 | } 109 | defer f.Close() 110 | update := func(cb func(s *Section)) {} 111 | s := bufio.NewScanner(f) 112 | for s.Scan() { 113 | line := strings.TrimSpace(s.Text()) 114 | if len(line) == 0 || line[0] == '#' { 115 | continue 116 | } 117 | parts := strings.Fields(line) 118 | if len(parts) > 1 && strings.ToLower(parts[0]) == "host" { 119 | hosts := parts[1:] 120 | for _, h := range hosts { 121 | if _, ok := HostInfo[h]; !ok { 122 | HostInfo[h] = Section{} 123 | } 124 | } 125 | update = func(cb func(s *Section)) { 126 | for _, h := range hosts { 127 | s, _ := HostInfo[h] 128 | cb(&s) 129 | HostInfo[h] = s 130 | } 131 | } 132 | } 133 | if len(parts) == 2 { 134 | switch strings.ToLower(parts[0]) { 135 | case "hostname": 136 | update(func(s *Section) { 137 | s.Hostname = parts[1] 138 | }) 139 | case "port": 140 | if p, err := strconv.Atoi(parts[1]); err == nil { 141 | update(func(s *Section) { 142 | s.Port = p 143 | }) 144 | } 145 | case "user": 146 | update(func(s *Section) { 147 | s.User = parts[1] 148 | }) 149 | case "identityfile": 150 | update(func(s *Section) { 151 | s.IdentityFile = parts[1] 152 | }) 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /sshhelper.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | rtop-bot - remote system monitoring bot 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "bufio" 30 | "bytes" 31 | "crypto/x509" 32 | "encoding/pem" 33 | "fmt" 34 | "golang.org/x/crypto/ssh" 35 | "golang.org/x/crypto/ssh/agent" 36 | "golang.org/x/crypto/ssh/terminal" 37 | "io/ioutil" 38 | "log" 39 | "net" 40 | "os" 41 | "os/signal" 42 | "strings" 43 | "syscall" 44 | ) 45 | 46 | func getpass(prompt string) (pass string, err error) { 47 | 48 | tstate, err := terminal.GetState(0) 49 | if err != nil { 50 | return 51 | } 52 | 53 | sig := make(chan os.Signal, 1) 54 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) 55 | go func() { 56 | quit := false 57 | for _ = range sig { 58 | quit = true 59 | break 60 | } 61 | terminal.Restore(0, tstate) 62 | if quit { 63 | fmt.Println() 64 | os.Exit(2) 65 | } 66 | }() 67 | defer func() { 68 | signal.Stop(sig) 69 | close(sig) 70 | }() 71 | 72 | f := bufio.NewWriter(os.Stdout) 73 | f.Write([]byte(prompt)) 74 | f.Flush() 75 | 76 | passbytes, err := terminal.ReadPassword(0) 77 | pass = string(passbytes) 78 | 79 | f.Write([]byte("\n")) 80 | f.Flush() 81 | 82 | return 83 | } 84 | 85 | // ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey. 86 | func ParsePemBlock(block *pem.Block) (interface{}, error) { 87 | 88 | switch block.Type { 89 | case "RSA PRIVATE KEY": 90 | return x509.ParsePKCS1PrivateKey(block.Bytes) 91 | case "EC PRIVATE KEY": 92 | return x509.ParseECPrivateKey(block.Bytes) 93 | case "DSA PRIVATE KEY": 94 | return ssh.ParseDSAPrivateKey(block.Bytes) 95 | default: 96 | return nil, fmt.Errorf("rtop-bot: unsupported key type %q", block.Type) 97 | } 98 | } 99 | 100 | func expandPath(path string) string { 101 | 102 | if len(path) < 2 || path[:2] != "~/" { 103 | return path 104 | } 105 | 106 | return strings.Replace(path, "~", currentUser.HomeDir, 1) 107 | } 108 | 109 | func addKeyAuth(auths []ssh.AuthMethod, keypath string) []ssh.AuthMethod { 110 | if len(keypath) == 0 { 111 | return auths 112 | } 113 | 114 | keypath = expandPath(keypath) 115 | 116 | // read the file 117 | pemBytes, err := ioutil.ReadFile(keypath) 118 | if err != nil { 119 | log.Print(err) 120 | os.Exit(1) 121 | } 122 | 123 | // get first pem block 124 | block, _ := pem.Decode(pemBytes) 125 | if block == nil { 126 | log.Printf("no key found in %s", keypath) 127 | return auths 128 | } 129 | 130 | // handle plain and encrypted keyfiles 131 | if x509.IsEncryptedPEMBlock(block) { 132 | log.Printf("warning: ignoring encrypted key '%s'", keypath) 133 | return auths 134 | } else { 135 | signer, err := ssh.ParsePrivateKey(pemBytes) 136 | if err != nil { 137 | log.Print(err) 138 | return auths 139 | } 140 | return append(auths, ssh.PublicKeys(signer)) 141 | } 142 | } 143 | 144 | func getAgentAuth() (auth ssh.AuthMethod, ok bool) { 145 | if sock := os.Getenv("SSH_AUTH_SOCK"); len(sock) > 0 { 146 | if agconn, err := net.Dial("unix", sock); err == nil { 147 | ag := agent.NewClient(agconn) 148 | auth = ssh.PublicKeysCallback(ag.Signers) 149 | ok = true 150 | } 151 | } 152 | return 153 | } 154 | 155 | func tryAgentConnect(user, addr string) (client *ssh.Client) { 156 | if auth, ok := getAgentAuth(); ok { 157 | config := &ssh.ClientConfig{ 158 | User: user, 159 | Auth: []ssh.AuthMethod{auth}, 160 | } 161 | client, _ = ssh.Dial("tcp", addr, config) 162 | } 163 | 164 | return 165 | } 166 | 167 | func sshConnect(user, addr, keypath string) (client *ssh.Client, err error) { 168 | // try connecting via agent first 169 | client = tryAgentConnect(user, addr) 170 | if client != nil { 171 | return 172 | } 173 | 174 | auths := make([]ssh.AuthMethod, 0, 2) 175 | auths = addKeyAuth(auths, keypath) 176 | 177 | config := &ssh.ClientConfig{ 178 | User: user, 179 | Auth: auths, 180 | } 181 | client, err = ssh.Dial("tcp", addr, config) 182 | if err != nil { 183 | log.Print(err) 184 | } 185 | 186 | return 187 | } 188 | 189 | func runCommand(client *ssh.Client, command string) (stdout string, err error) { 190 | session, err := client.NewSession() 191 | if err != nil { 192 | //log.Print(err) 193 | return 194 | } 195 | defer session.Close() 196 | 197 | var buf bytes.Buffer 198 | session.Stdout = &buf 199 | err = session.Run(command) 200 | if err != nil { 201 | //log.Print(err) 202 | return 203 | } 204 | stdout = string(buf.Bytes()) 205 | 206 | return 207 | } 208 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | rtop-bot - remote system monitoring bot 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "bufio" 30 | "golang.org/x/crypto/ssh" 31 | "strconv" 32 | "strings" 33 | "time" 34 | ) 35 | 36 | type FSInfo struct { 37 | MountPoint string 38 | Used uint64 39 | Free uint64 40 | } 41 | 42 | type Stats struct { 43 | Uptime time.Duration 44 | Hostname string 45 | Load1 string 46 | Load5 string 47 | Load10 string 48 | RunningProcs string 49 | TotalProcs string 50 | MemTotal uint64 51 | MemFree uint64 52 | MemBuffers uint64 53 | MemCached uint64 54 | SwapTotal uint64 55 | SwapFree uint64 56 | FSInfos []FSInfo 57 | } 58 | 59 | func getAllStats(client *ssh.Client, stats *Stats) { 60 | getUptime(client, stats) 61 | getHostname(client, stats) 62 | getLoad(client, stats) 63 | getMemInfo(client, stats) 64 | getFSInfo(client, stats) 65 | } 66 | 67 | func getUptime(client *ssh.Client, stats *Stats) (err error) { 68 | uptime, err := runCommand(client, "/bin/cat /proc/uptime") 69 | if err != nil { 70 | return 71 | } 72 | 73 | parts := strings.Fields(uptime) 74 | if len(parts) == 2 { 75 | var upsecs float64 76 | upsecs, err = strconv.ParseFloat(parts[0], 64) 77 | if err != nil { 78 | return 79 | } 80 | stats.Uptime = time.Duration(upsecs * 1e9) 81 | } 82 | 83 | return 84 | } 85 | 86 | func getHostname(client *ssh.Client, stats *Stats) (err error) { 87 | hostname, err := runCommand(client, "/bin/hostname -f") 88 | if err != nil { 89 | return 90 | } 91 | 92 | stats.Hostname = strings.TrimSpace(hostname) 93 | return 94 | } 95 | 96 | func getLoad(client *ssh.Client, stats *Stats) (err error) { 97 | line, err := runCommand(client, "/bin/cat /proc/loadavg") 98 | if err != nil { 99 | return 100 | } 101 | 102 | parts := strings.Fields(line) 103 | if len(parts) == 5 { 104 | stats.Load1 = parts[0] 105 | stats.Load5 = parts[1] 106 | stats.Load10 = parts[2] 107 | if i := strings.Index(parts[3], "/"); i != -1 { 108 | stats.RunningProcs = parts[3][0:i] 109 | if i+1 < len(parts[3]) { 110 | stats.TotalProcs = parts[3][i+1:] 111 | } 112 | } 113 | } 114 | 115 | return 116 | } 117 | 118 | func getMemInfo(client *ssh.Client, stats *Stats) (err error) { 119 | lines, err := runCommand(client, "/bin/cat /proc/meminfo") 120 | if err != nil { 121 | return 122 | } 123 | 124 | scanner := bufio.NewScanner(strings.NewReader(lines)) 125 | for scanner.Scan() { 126 | line := scanner.Text() 127 | parts := strings.Fields(line) 128 | if len(parts) == 3 { 129 | val, err := strconv.ParseUint(parts[1], 10, 64) 130 | if err != nil { 131 | continue 132 | } 133 | val *= 1024 134 | switch parts[0] { 135 | case "MemTotal:": 136 | stats.MemTotal = val 137 | case "MemFree:": 138 | stats.MemFree = val 139 | case "Buffers:": 140 | stats.MemBuffers = val 141 | case "Cached:": 142 | stats.MemCached = val 143 | case "SwapTotal:": 144 | stats.SwapTotal = val 145 | case "SwapFree:": 146 | stats.SwapFree = val 147 | } 148 | } 149 | } 150 | 151 | return 152 | } 153 | 154 | func getFSInfo(client *ssh.Client, stats *Stats) (err error) { 155 | lines, err := runCommand(client, "/bin/df -B1") 156 | if err != nil { 157 | return 158 | } 159 | 160 | scanner := bufio.NewScanner(strings.NewReader(lines)) 161 | flag := 0 162 | for scanner.Scan() { 163 | line := scanner.Text() 164 | parts := strings.Fields(line) 165 | n := len(parts) 166 | dev := n > 0 && strings.Index(parts[0], "/dev/") == 0 167 | if n == 1 && dev { 168 | flag = 1 169 | } else if (n == 5 && flag == 1) || (n == 6 && dev) { 170 | i := flag 171 | flag = 0 172 | used, err := strconv.ParseUint(parts[2-i], 10, 64) 173 | if err != nil { 174 | continue 175 | } 176 | free, err := strconv.ParseUint(parts[3-i], 10, 64) 177 | if err != nil { 178 | continue 179 | } 180 | stats.FSInfos = append(stats.FSInfos, FSInfo{ 181 | parts[5-i], used, free, 182 | }) 183 | } 184 | } 185 | 186 | return 187 | } 188 | --------------------------------------------------------------------------------