I am trying to generate PNG of goroutine profile with this demo, but it reports error parsing profile: unrecognized profile format. failed to fetch any source profiles
package main
import (
"fmt"
"log"
"os"
"os/exec"
"runtime/pprof"
"time"
)
func main() {
// start demo go routine
go func() {
for {
time.Sleep(time.Second)
}
}()
// get executable binary path
exePath, err := os.Executable()
if err != nil {
log.Println(err.Error())
return
}
log.Println("exePath", exePath)
// generate goroutine profile
profilePath := exePath + ".profile"
f, err := os.Create(profilePath)
if err != nil {
log.Println(err.Error())
return
}
defer f.Close()
if err := pprof.Lookup("goroutine").WriteTo(f, 2); err != nil {
log.Println(err.Error())
return
}
// generate PNG
pngPath := exePath + ".png"
result, err := exec.Command("go", "tool", "pprof", "-png", "-output", pngPath, exePath, profilePath).CombinedOutput()
if err != nil {
log.Println("make error:", err.Error())
}
log.Println("make result:", string(result))
}
You are using a debug value of 2 in pprof.Lookup("goroutine").WriteTo(f, 2). This will produce an output which the pprof tool can't parse, it is meant as directly human readable.
From the pprof.(*Profile).WriteTo docs:
The debug parameter enables additional output. Passing debug=0 writes the gzip-compressed protocol buffer described in https://github.com/google/pprof/tree/master/proto#overview. Passing debug=1 writes the legacy text format with comments translating addresses to function names and line numbers, so that a programmer can read the profile without tools.
The predefined profiles may assign meaning to other debug values; for example, when printing the "goroutine" profile, debug=2 means to print the goroutine stacks in the same form that a Go program uses when dying due to an unrecovered panic.
Changing this line to pprof.Lookup("goroutine").WriteTo(f, 0) resolves the issue.
Also, not relevant to the issue, but you might want to consider closing the file f.Close() before using it in the pprof command. WriteTo should flush the file to disk, but it can't hurt.
if err := pprof.Lookup("goroutine").WriteTo(f, 2); err != nil {
log.Println(err.Error())
return
}
if err := f.Close(); err != nil {
log.Println(err.Error())
return
}
// generate PNG
...
My code is supposed to SSH to a remote-host (let’s say Routers) and run multiple commands on the remote-host and return the outputs.
The code attached is simplified and has three parts:
Main function: Reads list of commands and then by using the ExecCommands function dials/ssh to a remote-host to execute the commands.
ExecCommands function takes the remote-host IP, list of commands and SSH ClientConfig that is used for SSH. Then it dials to the IP and run the commands one-by-one. At the end, returns the output of all commands in only one string
InsecureClientConfig function that actually doesn’t do much except creating a SSH ClientConfig which is used for ExecCommands function
This program works well when I just want to apply some commands or config and save the wholes result. I mean ExecCommands takes the bunch of commands, push all of them to the remote-host and returns (or saves) the whole output of applied commands in one string as output.
Problem:
I cannot process the output of each command individually. For example, assume that I apply CMD1, CMD2, CMD3, … to the remote-host#1 by using ExecCommands function. Since it gives me back the whole output in one string, it is hard to find which output belongs to which CMD
Goal:
Modify or re-design ExecCommands function to the way that it provides separate output for each command it applies. It means if for remote-host#1 it applies 10 commands, I should have 10 separate strings as output.
Conditions/Restrictions:
I can not create any extra session for commands and must apply all commands in the first SSH session I created, i.e. cannot create multiple Sessions and use Run, Shell, Output, Start function in SSH package
No re-authentication is allowed. For example, I have only a single one-time-password that can be used for all remote-hosts.
Remote hosts don't support "echo" like commands similar to what you have in Linux
The remote-hosts dont’s support any type of APIs
Points:
Main focus is the function ExecCommands. I put a simplified version of the whole code to give an idea
I am using stdout, err := session.StdoutPipe() to run multiple commands which means -as pipe - it's Reader only is possible to be read when the job is done.
An option is to use Session.Stdout and Session.Stdin inside of the for loop in ExecCommands function. Tried but was not successful.
Code:
package main
import (
"errors"
"fmt"
"io/ioutil"
"log"
"time"
"golang.org/x/crypto/ssh"
)
func main() {
// List of the commands should be sent to the devices
listCMDs := []string{
"set cli op-command-xml-output on",
"test routing fib-lookup virtual-router default ip 1.1.1.1",
"test routing fib-lookup virtual-router default ip 2.2.2.2",
"show interface ethernet1/1",
"show interface ethernet1/2",
"test security-policy-match protocol 6 source 1.1.1.1 destination 2.2.2.2 destination-port 443 from ZONE1 to ZONE2",
"test security-policy-match protocol 6 source 10.0.0.1 destination 10.0.2.1 destination-port 443 from ZONE1 to ZONE2",
"exit",
}
sshconfig := InsecureClientConfig("admin", "admin")
s, err := ExecCommands("192.168.1.250", listCMDs, sshconfig)
fmt.Println(s, err)
}
// ExecCommands ...
func ExecCommands(ipAddr string, commands []string, sshconfig *ssh.ClientConfig) (string, error) {
// Gets IP, credentials and config/commands, SSH Config (Timeout, Ciphers, ...) and returns
// output of the device as "string" and an error. If error == nil, means program was able to SSH with no issue
// Creating outerr as Output Error.
outerr := errors.New("nil")
outerr = nil
// Creating Output as String
var outputStr string
// Dial to the remote-host
client, err := ssh.Dial("tcp", ipAddr+":22", sshconfig)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create sesssion
session, err := client.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
// StdinPipee() returns a pipe that will be connected to the remote command's standard input when the command starts.
// StdoutPipe() returns a pipe that will be connected to the remote command's standard output when the command starts.
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start remote shell
err = session.Shell()
if err != nil {
log.Fatal(err)
}
// Send the commands to the remotehost one by one.
for _, cmd := range commands {
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
}
// Wait for session to finish
err = session.Wait()
if err != nil {
log.Fatal(err)
}
strByte, _ := ioutil.ReadAll(stdout)
outputStr = string(strByte)
return outputStr, outerr
}
// InsecureClientConfig ...
func InsecureClientConfig(userStr, passStr string) *ssh.ClientConfig {
SSHconfig := &ssh.ClientConfig{
User: userStr,
Timeout: 5 * time.Second,
Auth: []ssh.AuthMethod{ssh.Password(passStr)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Config: ssh.Config{
Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-cbc", "aes192-cbc",
"aes256-cbc", "3des-cbc", "des-cbc"},
KeyExchanges: []string{"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group14-sha1"},
},
}
return SSHconfig
}
This works properly:
package main
import (
"bufio"
"errors"
"fmt"
"log"
"time"
"golang.org/x/crypto/ssh"
)
func main() {
// List of the commands should be sent to the devices
listCMDs := []string{
"set cli op-command-xml-output on\n",
"test routing fib-lookup virtual-router default ip 1.1.1.1\n",
"test routing fib-lookup virtual-router default ip 2.2.2.2\n",
"show interface ethernet1/1\n",
"show interface ethernet1/2\n",
"test security-policy-match protocol 6 source 1.1.1.1 destination 2.2.2.2 destination-port 443 from ZONE1 to ZONE2\n",
"test security-policy-match protocol 6 source 10.0.0.1 destination 10.0.2.1 destination-port 443 from ZONE1 to ZONE2\n",
"exit",
}
sshconfig := InsecureClientConfig("admin", "Ghazanfar1!")
s, _ := ExecCommands("192.168.1.249", listCMDs, sshconfig)
for _, item := range s {
fmt.Println(item)
fmt.Println("-------------------------------")
}
}
// ExecCommands ...
func ExecCommands(ipAddr string, commands []string, sshconfig *ssh.ClientConfig) ([]string, error) {
// Gets IP, credentials and config/commands, SSH Config (Timeout, Ciphers, ...) and returns
// output of the device as "string" and an error. If error == nil, means program was able to SSH with no issue
// Creating outerr as Output Error.
outerr := errors.New("nil")
outerr = nil
// Creating Output as String
var outputStr []string
var strTmp string
// Dial to the remote-host
client, err := ssh.Dial("tcp", ipAddr+":22", sshconfig)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create sesssion
session, err := client.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
// StdinPipee() returns a pipe that will be connected to the remote command's standard input when the command starts.
// StdoutPipe() returns a pipe that will be connected to the remote command's standard output when the command starts.
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start remote shell
err = session.Shell()
if err != nil {
log.Fatal(err)
}
stdinLines := make(chan string)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
stdinLines <- scanner.Text()
}
if err := scanner.Err(); err != nil {
log.Printf("scanner failed: %v", err)
}
close(stdinLines)
}()
// Send the commands to the remotehost one by one.
for i, cmd := range commands {
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
if i == len(commands)-1 {
_ = stdin.Close() // send eof
}
// wait for command to complete
// we'll assume the moment we've gone 1 secs w/o any output that our command is done
timer := time.NewTimer(0)
InputLoop:
for {
timer.Reset(time.Second)
select {
case line, ok := <-stdinLines:
if !ok {
log.Println("Finished processing")
break InputLoop
}
strTmp += line
strTmp += "\n"
case <-timer.C:
break InputLoop
}
}
outputStr = append(outputStr, strTmp)
//log.Printf("Finished processing %v\n", cmd)
strTmp = ""
}
// Wait for session to finish
err = session.Wait()
if err != nil {
log.Fatal(err)
}
return outputStr, outerr
}
// InsecureClientConfig ...
func InsecureClientConfig(userStr, passStr string) *ssh.ClientConfig {
SSHconfig := &ssh.ClientConfig{
User: userStr,
Timeout: 5 * time.Second,
Auth: []ssh.AuthMethod{ssh.Password(passStr)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Config: ssh.Config{
Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-cbc", "aes192-cbc",
"aes256-cbc", "3des-cbc", "des-cbc"},
KeyExchanges: []string{"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group14-sha1"},
},
}
return SSHconfig
}
Since you have limited number of commands to run on special hardwares and you know the pattern of the each command's output, you may use strings.Split or regexp to split the output.
And if you do not have echo command, but know any command with fast response with unique output pattern, then you may replace it with echo command in the following example (number 2).
Since a session only accepts one call to Run, Start, Shell, Output, or CombinedOutput, and you do not want to start a new session per command:
The key is to use a strings.Builder and empty it using sb.Reset() befor sending the command, and using io.Copy to copy concurrently the session's stdout into strings.Builder (assuming you do not need session's stderr):
sb := new(strings.Builder)
go io.Copy(sb, stdout)
This works if you know how much to wait for each command (tested):
sb := new(strings.Builder)
go io.Copy(sb, stdout)
commands := []string{"uname -a", "sleep 1", "pwd", "whoami", "exit"}
wait := []time.Duration{10, 1200, 20, 10, 10} // * time.Millisecond
ans := []string{}
time.Sleep(10 * time.Millisecond) // wait for the ssh greetings
// Send the commands to the remotehost one by one.
for i, cmd := range commands {
sb.Reset()
fmt.Println("*** command:\t", cmd)
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
time.Sleep(wait[i] * time.Millisecond) // wait for the command to finish
s := sb.String()
fmt.Println("*** response:\t", s)
ans = append(ans, s)
}
Using string delimiter and strings.Split (Note: You may replace echo with any fast command with known output pattern):
sb := new(strings.Builder)
go io.Copy(sb, stdout)
commands := []string{"uname -a", "sleep 1", "pwd", "whoami"}
delim := "********--------========12345678"
for _, cmd := range commands {
_, err = stdin.Write([]byte("echo " + delim + "\n"))
if err != nil {
log.Fatal(err)
}
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
}
_, err = stdin.Write([]byte("exit\n"))
if err != nil {
log.Fatal(err)
}
err = session.Wait() // Wait for session to exit
if err != nil {
log.Fatal(err)
}
ans := strings.Split(sb.String(), delim)
ans = ans[1:] // remove ssh greetings
Check this out: https://github.com/yahoo/vssh
You can set sessions to how many commands you need to run concurrently then send each command to remote host through run method and get the result individually!
i have the following code
package main
import (
"os/exec"
"fmt"
"os"
)
func main() {
cmd := exec.Command("systemctl", "check", "sshd")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Cannot find process")
os.Exit(1)
}
fmt.Printf("Status is: %s", string(out))
fmt.Println("Starting Role")
If the service is down, program will exit, althrough i would like to get its status ( 'down' , 'inactive', etc)
If the service is up, program will not exit and will print ' active ' output
Any hints, please ?
You're exiting if exec.Command returns an error, but you're not checking the type of error returned.
Per the docs:
If the command starts but does not complete successfully, the error is
of type *ExitError. Other error types may be returned for other
situations.
Rather than just exiting, you should check if the error corresponds to a non-zero exit code from systemctl or a problem running it. This can be done with the following:
func main() {
cmd := exec.Command("systemctl", "check", "sshd")
out, err := cmd.CombinedOutput()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Printf("systemctl finished with non-zero: %v\n", exitErr)
} else {
fmt.Printf("failed to run systemctl: %v", err)
os.Exit(1)
}
}
fmt.Printf("Status is: %s\n", string(out))
}
I need a way to read input from console twice (1 -from cat outupt, 2 - user inputs password), like this:
cat ./test_data/test.txt | app account import
My current code skips password input:
reader := bufio.NewReader(os.Stdin)
raw, err := ioutil.ReadAll(reader)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
wlt, err := wallet.Deserialize(string(raw))
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
fmt.Print("Enter password: ")
pass := ""
fmt.Fscanln(reader, &pass)
Also tried to read password with Scanln - doesn't works.
Note:
cat (and piping at all) can't be used with user input, as shell redirects inputs totally.
So the most simple solutions are:
to pass filename as argument
redirect manually app account import < ./test.txt
Read file and password separately (and don't show password), try this:
package main
import (
"fmt"
"io/ioutil"
"log"
"github.com/howeyc/gopass"
)
func main() {
b, err := ioutil.ReadFile("./test_data/test.txt") // just pass the file name
if err != nil {
fmt.Print(err)
}
fmt.Println(string(b))
fmt.Print("Password: ")
pass, err := gopass.GetPasswd()
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(pass))
}
and go get github.com/howeyc/gopass first.
This question is about golang.org/x/crypto/ssh package and maybe pseudo-terminal behaviour.
The code
Here is the demo code. You can run it on your local machine just change credentials to access SSH.
package main
import (
"bufio"
"fmt"
"golang.org/x/crypto/ssh"
"io"
)
func main() {
var pipe io.Reader
whichPipe := "error" // error or out
address := "192.168.1.62:22"
username := "username"
password := "password"
sshConfig := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{ssh.Password(password)},
}
connection, err := ssh.Dial("tcp", address, sshConfig)
if err != nil {
panic(err)
}
session, err := connection.NewSession()
if err != nil {
panic(err)
}
modes := ssh.TerminalModes{
ssh.ECHO: 0,
ssh.ECHOCTL: 0,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
if err := session.RequestPty("xterm", 80, 0, modes); err != nil {
session.Close()
panic(err)
}
switch whichPipe {
case "error":
pipe, _ = session.StderrPipe()
case "out":
pipe, _ = session.StdoutPipe()
}
err = session.Run("whoami23")
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}
Actual result
Empty line
Expected result
bash: whoami23: command not found
Current "solution"
To get expected result you have two options:
Change whichPipe value to out. Yes, all errors going to stdout in case if you use tty.
Remove session.RequestPty. But in my case, I need to run sudo commands which require tty (servers are out of my control so I can't disable this requirement).
I use third way. I check err from err = session.Run("whoami23") and if it's not nil I mark content of session.StdoutPipe() as STDERR one.
But this method has limits. For example, if I run something like sudo sh -c 'uname -r; whoami23;' the whole result will be marked as error while uname -r returns output to STDOUT.
The question
While the behaviour looks logical to me (all that SSH client sees from pty is output without differentiations) I'm still not sure if I may miss something and there is a trick that allows to split these outputs.