// NewSSHCommand is the required initializer for SSHCommand. func NewSSHCommand(log logging.Logger, opts SSHCommandOpts) (*SSHCommand, error) { usr, err := user.Current() if err != nil { return nil, err } klientKite, err := klient.CreateKlientWithDefaultOpts() if err != nil { return nil, err } if err := klientKite.Dial(); err != nil { log.New("NewSSHCommand").Error("Dialing local klient failed. err:%s", err) return nil, ErrLocalDialingFailed } k := klient.NewKlient(klientKite) return &SSHCommand{ Klient: k, Log: log.New("SSHCommand"), Ask: opts.Ask, Debug: opts.Debug, SSHKey: &SSHKey{ Log: log.New("SSHKey"), Debug: opts.Debug, RemoteUsername: opts.RemoteUsername, KeyPath: path.Join(usr.HomeDir, config.SSHDefaultKeyDir), KeyName: config.SSHDefaultKeyName, Klient: k, }, }, nil }
// CheckLocal runs several diagnostics on the local Klient. Errors // indicate an unhealthy or not running Klient, and can be compare to // the ErrHealth* types. // // TODO: Possibly return a set of warnings too? If we have any.. func (c *HealthChecker) LocalRequirements() error { res, err := c.HTTPClient.Get(c.LocalKlientAddress) // If there was an error even talking to Klient, something is wrong. if err != nil { return ErrHealthNoHTTPReponse{Message: fmt.Sprintf( "local klient /kite route is returning an error: %s", err, )} } defer res.Body.Close() switch res.StatusCode { case http.StatusOK, http.StatusNoContent: default: return ErrHealthUnexpectedResponse{Message: fmt.Sprintf( "unexpected status code: %d", res.StatusCode, )} } if res.StatusCode == http.StatusOK { // It should be safe to ignore any errors dumping the response data, // since we just want to check the data itself. Handling the error // might aid with debugging any problems though. p, err := ioutil.ReadAll(res.Body) if err != nil { return ErrHealthUnexpectedResponse{Message: fmt.Sprintf( "failure reading local klient /kite response: %s", err, )} } if bytes.Compare(kiteHTTPResponse, bytes.TrimSpace(p)) != 0 { return ErrHealthUnexpectedResponse{Message: fmt.Sprintf( "local klient /kite route is returning an unexpected response: %s", p, )} } } // The only error CreateKlientClient returns (currently) is kite read // error, so we can handle that. k, err := klient.CreateKlientWithDefaultOpts() if err != nil { return ErrHealthUnreadableKiteKey{Message: fmt.Sprintf( "klient kite key is unable to be read: %s", err, )} } // TODO: Identify varing Dial errors to produce meaningful health // responses. if err = k.Dial(); err != nil { return ErrHealthDialFailed{Message: fmt.Sprintf( "dailing local klient failed: %s", err, )} } return nil }
// NewRunCommand is the required initializer for RunCommand. func NewRunCommand() (*RunCommand, error) { klientKite, err := klient.CreateKlientWithDefaultOpts() if err != nil { return nil, err } if err := klientKite.Dial(); err != nil { return nil, err } return &RunCommand{Transport: klientKite}, nil }
// ListCommand returns list of remote machines belonging to user or that can be // accessed by the user. func ListCommand(c *cli.Context, log logging.Logger, _ string) int { if len(c.Args()) != 0 { cli.ShowCommandHelp(c, "list") return 1 } showAll := c.Bool("all") k, err := klient.CreateKlientWithDefaultOpts() if err != nil { log.Error("Error creating klient client. err:%s", err) fmt.Println(defaultHealthChecker.CheckAllFailureOrMessagef(GenericInternalError)) return 1 } if err := k.Dial(); err != nil { log.Error("Error dialing klient client. err:%s", err) fmt.Println(defaultHealthChecker.CheckAllFailureOrMessagef(GenericInternalError)) return 1 } infos, err := getListOfMachines(k) if err != nil { log.Error("Error listing machines. err:%s", err) fmt.Println(getListErrRes(err, defaultHealthChecker)) return 1 } // Sort our infos sort.Sort(infos) // Filter out infos for listing and json. for i := 0; i < len(infos); i++ { info := &infos[i] onlineRecently := time.Since(info.OnlineAt) <= 24*time.Hour hasMounts := len(info.Mounts) > 0 // Do not show machines that have been offline for more than 24h, // but only if the machine doesn't have any mounts and we aren't using the --all // flag. if !hasMounts && !showAll && !onlineRecently { // Remove this element from the slice, because we're not showing it as // described above. infos = append(infos[:i], infos[i+1:]...) // Decrement the index, since we're removing the item from the slice. i-- continue } // For a more clear UX, replace the team name of the default Koding team, // with Koding.com for i, team := range info.Teams { if team == "Koding" { info.Teams[i] = "koding.com" } } switch info.MachineStatus { case machine.MachineOffline: info.MachineStatusName = "offline" case machine.MachineOnline: info.MachineStatusName = "online" case machine.MachineDisconnected: info.MachineStatusName = "disconnected" case machine.MachineConnected: info.MachineStatusName = "connected" case machine.MachineError: info.MachineStatusName = "error" case machine.MachineRemounting: info.MachineStatusName = "remounting" default: info.MachineStatusName = "unknown" } } if c.Bool("json") { jsonBytes, err := json.MarshalIndent(infos, "", " ") if err != nil { log.Error("Marshalling infos to json failed. err:%s", err) fmt.Println(GenericInternalError) return 1 } fmt.Println(string(jsonBytes)) return 0 } w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0) fmt.Fprintf(w, "\tTEAM\tLABEL\tIP\tALIAS\tSTATUS\tMOUNTED PATHS\n") for i, info := range infos { // Join multiple teams into a single identifier team := strings.Join(info.Teams, ",") var formattedMount string if len(info.Mounts) > 0 { formattedMount += fmt.Sprintf( "%s -> %s", shortenPath(info.Mounts[0].LocalPath), shortenPath(info.Mounts[0].RemotePath), ) } // Currently we are displaying the status message over the formattedMount, // if it exists. if info.StatusMessage != "" { formattedMount = info.StatusMessage } fmt.Fprintf(w, " %d.\t%s\t%s\t%s\t%s\t%s\t%s\n", i+1, team, info.MachineLabel, info.IP, info.VMName, info.MachineStatusName, formattedMount, ) } w.Flush() return 0 }
// List retrieves user's machines from kloud. func List(options *ListOptions) ([]*Info, error) { var ( listReq = stack.MachineListRequest{} listRes = stack.MachineListResponse{} ) // Get info from kloud. if err := kloud.Call("machine.list", &listReq, &listRes); err != nil { return nil, err } // Register machines to klient and get aliases. // // TODO(ppknap): this is copied from klientctl old list and will be reworked. k, err := klient.CreateKlientWithDefaultOpts() if err != nil { fmt.Fprintln(os.Stderr, "Error creating klient:", err) return nil, err } if err := k.Dial(); err != nil { fmt.Fprintln(os.Stderr, "Error dialing klient:", err) return nil, err } createReq := machinegroup.CreateRequest{ Addresses: make(map[kmachine.ID][]kmachine.Addr), } for _, m := range listRes.Machines { createReq.Addresses[kmachine.ID(m.ID)] = []kmachine.Addr{ { Network: "ip", Value: m.IP, UpdatedAt: time.Now(), }, { Network: "kite", Value: m.QueryString, UpdatedAt: time.Now(), }, { Network: "http", Value: m.RegisterURL, UpdatedAt: time.Now(), }, } } createRaw, err := k.Tell("machine.create", createReq) if err != nil { return nil, err } createRes := machinegroup.CreateResponse{} if err := createRaw.Unmarshal(&createRes); err != nil { return nil, err } infos := make([]*Info, len(listRes.Machines)) for i, m := range listRes.Machines { infos[i] = &Info{ ID: m.ID, Alias: createRes.Aliases[kmachine.ID(m.ID)], Team: m.Team, Stack: m.Stack, Provider: m.Provider, Label: m.Label, IP: m.IP, QueryString: m.QueryString, RegisterURL: m.RegisterURL, CreatedAt: m.CreatedAt, Status: kmachine.MergeStatus(kmachine.Status{ State: fromMachineStateString(m.Status.State), Reason: m.Status.Reason, Since: m.Status.ModifiedAt, }, createRes.Statuses[kmachine.ID(m.ID)]), Username: machineUserFromUsers(m.Users), Owner: ownerFromUsers(m.Users), } } // Sort items before we return. sort.Sort(InfoSlice(infos)) return infos, nil }
// SSH connects to remote machine using SSH protocol. func SSH(options *SSHOptions) error { // Translate identifier to machine ID. // // TODO(ppknap): this is copied from klientctl old list and will be reworked. k, err := klient.CreateKlientWithDefaultOpts() if err != nil { fmt.Fprintln(os.Stderr, "Error creating klient:", err) return err } if err := k.Dial(); err != nil { fmt.Fprintln(os.Stderr, "Error dialing klient:", err) return err } idReq := machinegroup.IDRequest{ Identifier: options.Identifier, } idRaw, err := k.Tell("machine.id", idReq) if err != nil { return err } idRes := machinegroup.IDResponse{} if err := idRaw.Unmarshal(&idRes); err != nil { return err } // Get local public key in case we need to copy it to remote machine. path, err := ssh.GetKeyPath(nil) if err != nil { return err } pubPath, privPath, err := ssh.KeyPaths(path) if err != nil { return err } pubkey, err := ssh.PublicKey(pubPath) if err != nil && err != ssh.ErrPublicKeyNotFound { return err } // Generate new key pair if it does not exist. if err == ssh.ErrPublicKeyNotFound { if pubkey, _, err = ssh.GenerateSaved(pubPath, privPath); err != nil { return err } } // Add created key to authorized hosts on remote machine. sshReq := machinegroup.SSHRequest{ ID: idRes.ID, Username: options.Username, PublicKey: pubkey, } sshRaw, err := k.Tell("machine.ssh", sshReq) if err != nil { return err } sshRes := machinegroup.SSHResponse{} if err := sshRaw.Unmarshal(&sshRes); err != nil { return err } // TODO(ppknap): move this to ssh package. args := []string{ "-i", privPath, "-o", "StrictHostKeychecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "ServerAliveInterval=300", "-o", "ServerAliveCountMax=3", "-o", "ConnectTimeout=7", "-o", "ConnectionAttempts=1", sshRes.Username + "@" + sshRes.Host, } if sshRes.Port > 0 { args = append(args, "-p", strconv.Itoa(sshRes.Port)) } options.Log.Info("Executing command: ssh %s", strings.Join(args, " ")) cmd := exec.Command("ssh", args...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr return cmd.Run() }