Skip to content
Snippets Groups Projects
main.go 6.96 KiB
Newer Older
  • Learn to ignore specific revisions
  • kayrus's avatar
    kayrus committed
    package main
    
    import (
    	"crypto/rsa"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"os/user"
    	"path/filepath"
    
    	"github.com/google/uuid"
    	"github.com/gophercloud/gophercloud"
    	"github.com/gophercloud/gophercloud/acceptance/clients"
    	"github.com/gophercloud/gophercloud/openstack"
    	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
    	"github.com/howeyc/gopass"
    	"github.com/kayrus/putty"
    	env "github.com/sapcc/cloud-env"
    	"github.com/spf13/cobra"
    	"github.com/spf13/viper"
    	"golang.org/x/crypto/ssh"
    )
    
    const MaxKeySize = 10240
    
    
    kayrus's avatar
    kayrus committed
    var Version string
    
    
    kayrus's avatar
    kayrus committed
    // RootCmd represents the base command when called without any subcommands
    var RootCmd = &cobra.Command{
    	Use:          "nova-password <server-name>|<server-id> [<server-name>|<server-id>...]",
    	Short:        "Get the admin password for an OpenStack server",
    	SilenceUsage: true,
    
    kayrus's avatar
    kayrus committed
    	Version:      Version,
    
    kayrus's avatar
    kayrus committed
    	PreRunE: func(cmd *cobra.Command, args []string) error {
    		if len(args) == 0 {
    			cmd.Usage()
    			return fmt.Errorf("server name has to be provided")
    		}
    		return nil
    	},
    	RunE: func(cmd *cobra.Command, args []string) error {
    
    		// get the wait timeout
    		wait := viper.GetUint("wait")
    
    kayrus's avatar
    kayrus committed
    		// Convert Unix path type to Windows path type, when necessary
    		keyPath := filepath.FromSlash(viper.GetString("private-key-path"))
    
    		fmt.Printf("private-key-path: %s\n", keyPath)
    
    		// Read the key
    		key, err := readKey(keyPath)
    		if err != nil {
    			return err
    		}
    
    		var privateKey interface{}
    
    		puttyKey, pkerr := putty.New(key)
    		if pkerr == nil {
    			if puttyKey.Algo != "ssh-rsa" {
    				return fmt.Errorf("unsupported key type %s\nOnly RSA PKCS #1 v1.5 is supported by OpenStack", puttyKey.Algo)
    			}
    			// parse putty key
    			if puttyKey.Encryption != "none" {
    				// If the key is encrypted, decrypt it
    				log.Print("Private key is encrypted with the password")
    				fmt.Print("Enter the password: ")
    				pass, err := gopass.GetPasswd()
    				if err != nil {
    					return err
    				}
    				privateKey, err = puttyKey.ParseRawPrivateKey(pass)
    				if err != nil {
    					return err
    				}
    			} else {
    				privateKey, err = puttyKey.ParseRawPrivateKey(nil)
    				if err != nil {
    					return err
    				}
    			}
    		} else {
    			// Parse the pem key
    			privateKey, err = ssh.ParseRawPrivateKey(key)
    			if err != nil {
    				if err.Error() != "ssh: no key found" {
    					// If the key is encrypted, decrypt it
    					log.Print("Private key is encrypted with the password")
    					fmt.Print("Enter the password: ")
    					pass, err := gopass.GetPasswd()
    					if err != nil {
    						return err
    					}
    
    					privateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(key, pass)
    					if err != nil {
    						return err
    					}
    				} else {
    					if pkerr != nil {
    						log.Print(pkerr)
    					}
    					return err
    				}
    			}
    		}
    
    
    		var errors []error
    
    
    kayrus's avatar
    kayrus committed
    		switch v := privateKey.(type) {
    		// Only RSA PKCS #1 v1.5 is supported by OpenStack
    		case *rsa.PrivateKey:
    			// Initialize the compute client
    			client, err := newComputeV2()
    			if err != nil {
    				return err
    			}
    
    			for _, server := range args {
    
    				err = processServer(client, server, v, wait)
    
    kayrus's avatar
    kayrus committed
    				if err != nil {
    					log.Printf("%s", err)
    
    					errors = append(errors, err)
    
    kayrus's avatar
    kayrus committed
    				}
    			}
    		default:
    			return fmt.Errorf("unsupported key type %T\nOnly RSA PKCS #1 v1.5 is supported by OpenStack", v)
    		}
    
    
    		if len(errors) > 0 {
    			return fmt.Errorf("%v", errors)
    		}
    
    
    kayrus's avatar
    kayrus committed
    		return nil
    	},
    }
    
    // Execute adds all child commands to the root command sets flags appropriately.
    // This is called by main.main(). It only needs to happen once to the rootCmd.
    func main() {
    	initRootCmdFlags()
    	if err := RootCmd.Execute(); err != nil {
    		os.Exit(1)
    	}
    }
    
    func initRootCmdFlags() {
    	// Get the current user home dir
    	usr, err := user.Current()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Set default key path
    	defaultKeyPath := filepath.FromSlash(usr.HomeDir + "/.ssh/id_rsa")
    
    	// debug flag
    	RootCmd.PersistentFlags().BoolP("debug", "d", false, "print out request and response objects")
    	RootCmd.PersistentFlags().StringP("private-key-path", "i", defaultKeyPath, "a path to the RSA private key (PuTTY and OpenSSH formats)")
    
    	RootCmd.PersistentFlags().UintP("wait", "w", 0, "wait for the password timeout in seconds")
    
    kayrus's avatar
    kayrus committed
    	viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug"))
    	viper.BindPFlag("private-key-path", RootCmd.PersistentFlags().Lookup("private-key-path"))
    
    	viper.BindPFlag("wait", RootCmd.PersistentFlags().Lookup("wait"))
    
    kayrus's avatar
    kayrus committed
    }
    
    // newComputeV2 creates a ServiceClient that may be used with the v2 compute
    // package.
    func newComputeV2() (*gophercloud.ServiceClient, error) {
    	ao, err := env.AuthOptionsFromEnv()
    	if err != nil {
    		return nil, err
    	}
    
    	client, err := openstack.NewClient(ao.IdentityEndpoint)
    	if err != nil {
    		return nil, err
    	}
    
    	if viper.GetBool("debug") {
    		client.HTTPClient = http.Client{
    			Transport: &clients.LogRoundTripper{
    				Rt: &http.Transport{},
    			},
    		}
    	}
    
    	err = openstack.Authenticate(client, ao)
    	if err != nil {
    		return nil, err
    	}
    
    	return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
    		Region: env.Get("OS_REGION_NAME"),
    	})
    }
    
    
    func processServer(client *gophercloud.ServiceClient, server string, privateKey *rsa.PrivateKey, wait uint) error {
    
    kayrus's avatar
    kayrus committed
    	// Verify whether UUID was provided. If the name was provided, resolve the server name
    	_, err := uuid.Parse(server)
    	if err != nil {
    		fmt.Printf("server: %s", server)
    		server, err = servers.IDFromName(client, server)
    		if err != nil {
    			fmt.Println()
    			return err
    		}
    		fmt.Printf(" (%s)\n", server)
    	} else {
    		fmt.Printf("server: %s\n", server)
    	}
    
    
    	var res servers.GetPasswordResult
    	if wait > 0 {
    		log.Printf("Waiting for %d seconds to get the password", wait)
    		// Wait for the encrypted server password
    		res, err = waitForPassword(client, server, wait)
    		if err != nil {
    			return err
    		}
    	} else {
    		// Get the encrypted server password
    		res = servers.GetPassword(client, server)
    	}
    
    	if res.Err != nil {
    		return res.Err
    
    kayrus's avatar
    kayrus committed
    	}
    
    	// Decrypt the password
    
    	pwd, err := res.ExtractPassword(privateKey)
    
    kayrus's avatar
    kayrus committed
    	if err != nil {
    		return err
    	}
    	fmt.Printf("Decrypted compute instance password: %s\n", pwd)
    
    	return nil
    }
    
    
    func waitForPassword(c *gophercloud.ServiceClient, id string, secs uint) (servers.GetPasswordResult, error) {
    	var res servers.GetPasswordResult
    	err := gophercloud.WaitFor(int(secs), func() (bool, error) {
    		var err error
    		res = servers.GetPassword(c, id)
    		if res.Err != nil {
    			return false, res.Err
    		}
    
    		pass, err := res.ExtractPassword(nil)
    		if err != nil {
    			return false, err
    		}
    
    		if pass == "" {
    			return false, nil
    		}
    
    		return true, nil
    	})
    
    	return res, err
    }
    
    
    kayrus's avatar
    kayrus committed
    func readKey(path string) ([]byte, error) {
    	f, err := os.Open(filepath.FromSlash(path))
    	if err != nil {
    		return nil, err
    	}
    
    	defer f.Close()
    
    	stat, err := f.Stat()
    	if err != nil {
    		return nil, err
    	}
    
    	size := stat.Size()
    	if size > MaxKeySize {
    		return nil, fmt.Errorf("Invalid key size: %d bytes", size)
    	}
    
    	if size == 0 {
    		// force to use "MaxKeySize", when detected file size is 0 (e.g. /dev/stdin)
    		size = MaxKeySize
    	}
    
    	key := make([]byte, size)
    
    	// Read the key
    	_, err = f.Read(key)
    	if err != nil {
    		return nil, err
    	}
    
    	return key, nil
    }