├── .gitignore ├── VERSION ├── lib ├── flag.go ├── log.go ├── config_test.go └── config.go ├── command ├── version.go ├── nocommand.go ├── logout.go ├── command.go ├── ssh.go ├── vps.go ├── vps_label.go ├── vps_power.go ├── ssh_key.go ├── login.go ├── vps_remove.go ├── vps_list.go ├── vps_stat.go └── vps_add.go ├── Makefile ├── .travis.yml ├── main.go ├── cpanel └── browser.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v20150223.2 2 | -------------------------------------------------------------------------------- /lib/flag.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import () 4 | -------------------------------------------------------------------------------- /command/version.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/hironobu-s/conoha-vps/lib" 5 | ) 6 | 7 | type Version struct { 8 | *Command 9 | } 10 | 11 | func NewVersion() *Version { 12 | return &Version{ 13 | Command: NewCommand(), 14 | } 15 | } 16 | 17 | func (cmd *Version) parseFlag() error { 18 | return nil 19 | } 20 | 21 | func (cmd *Version) Usage() { 22 | } 23 | 24 | func (cmd *Version) Run() error { 25 | println(lib.Version) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell cat VERSION) 2 | NAME=conoha 3 | BINDIR=bin 4 | GOARCH=amd64 5 | GOFLAGS=-ldflags "-X github.com/hironobu-s/conoha-vps/lib.Version $(VERSION)" 6 | 7 | all: clean windows darwin linux 8 | 9 | windows: 10 | GOOS=$@ GOARCH=$(GOARCH) go build $(GOFLAGS) -o $(BINDIR)/$@/$(NAME).exe 11 | cd bin/$@; zip $(NAME).$(GOARCH).zip $(NAME).exe 12 | 13 | darwin: 14 | GOOS=$@ GOARCH=$(GOARCH) go build $(GOFLAGS) -o $(BINDIR)/$@/$(NAME) 15 | cd bin/$@; gzip -c $(NAME) > $(NAME)-osx.$(GOARCH).gz 16 | 17 | linux: 18 | GOOS=$@ GOARCH=$(GOARCH) go build $(GOFLAGS) -o $(BINDIR)/$@/$(NAME) 19 | cd bin/$@; gzip -c $(NAME) > $(NAME)-linux.$(GOARCH).gz 20 | 21 | clean: 22 | rm -rf $(BINDIR) 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | before_install: 3 | - go get github.com/PuerkitoBio/goquery 4 | - go get github.com/Sirupsen/logrus 5 | - go get github.com/howeyc/gopass 6 | - go get github.com/ogier/pflag 7 | - go get github.com/mitchellh/gox 8 | - go get github.com/mitchellh/go-homedir 9 | - gox -build-toolchain -osarch="darwin/amd64 linux/amd64 windows/amd64" 10 | 11 | script: make 12 | deploy: 13 | provider: releases 14 | api_key: 15 | secure: Z55l9blwOv870mSOatQIKDkKL8UMBfuvTOj+RYyPC80W+IoAspomgS9yK2dHwcWoR2n2M5WwCLRcKYqQu5UHpqeGr4LnmlhyeWoA0fbRqYq+4IRG/xC6qfp99r9NTKjW9GHz7ThRiJBTrYbVve5Rc8wnD9SmAr5zSXufMseo/KU= 16 | file: 17 | - bin/windows/conoha.amd64.zip 18 | - bin/darwin/conoha-osx.amd64.gz 19 | - bin/linux/conoha-linux.amd64.gz 20 | 21 | skip_cleanup: true 22 | on: 23 | tags: true 24 | repo: hironobu-s/conoha-vps 25 | all_branches: true 26 | -------------------------------------------------------------------------------- /command/nocommand.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Nocommand struct { 8 | *Command 9 | } 10 | 11 | func NewNocommand() *Nocommand { 12 | return &Nocommand{ 13 | Command: NewCommand(), 14 | } 15 | } 16 | 17 | func (cmd *Nocommand) parseFlag() error { 18 | return nil 19 | } 20 | 21 | func (cmd *Nocommand) Usage() { 22 | fmt.Println(`Usage: conoha COMMAND [OPTIONS] 23 | 24 | DESCRIPTION 25 | A CLI-Tool for ConoHa VPS. 26 | 27 | COMMANDS 28 | add Add VPS. 29 | label Change VPS label. 30 | list List VPS. 31 | login Authenticate an account. 32 | logout Remove an authenticate file(~/.conoha-vps). 33 | power Send power-command to VPS. 34 | remove Remove VPS. 35 | ssh-key Download and store SSH Private key. 36 | ssh Login to VPS via SSH. 37 | stat Display VPS information. 38 | version Display version. 39 | `) 40 | } 41 | 42 | func (cmd *Nocommand) Run() error { 43 | cmd.Usage() 44 | return &ShowUsageError{} 45 | } 46 | -------------------------------------------------------------------------------- /lib/log.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/Sirupsen/logrus" 7 | "sort" 8 | ) 9 | 10 | func init() { 11 | } 12 | 13 | var instance *logrus.Logger 14 | 15 | func GetLogInstance() *logrus.Logger { 16 | if instance == nil { 17 | // ログレベルの設定 18 | instance = logrus.New() 19 | 20 | instance.Level = logrus.InfoLevel 21 | //instance.Formatter = &SimpleFormatter{} 22 | instance.Formatter = &logrus.TextFormatter{} 23 | } 24 | return instance 25 | } 26 | 27 | type SimpleFormatter struct { 28 | } 29 | 30 | func (f *SimpleFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) { 31 | switch value.(type) { 32 | case string, error: 33 | fmt.Fprintf(b, "%v=%q ", key, value) 34 | default: 35 | fmt.Fprintf(b, "%v=%v ", key, value) 36 | } 37 | } 38 | 39 | func (f *SimpleFormatter) Format(entry *logrus.Entry) ([]byte, error) { 40 | 41 | var keys []string 42 | for k := range entry.Data { 43 | keys = append(keys, k) 44 | } 45 | sort.Strings(keys) 46 | 47 | b := &bytes.Buffer{} 48 | 49 | fmt.Fprintf(b, "%s", entry.Message) 50 | 51 | b.WriteByte('\n') 52 | return b.Bytes(), nil 53 | 54 | } 55 | -------------------------------------------------------------------------------- /command/logout.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hironobu-s/conoha-vps/lib" 6 | flag "github.com/ogier/pflag" 7 | "os" 8 | ) 9 | 10 | type Logout struct { 11 | *Command 12 | } 13 | 14 | func NewLogout() *Logout { 15 | return &Logout{ 16 | Command: NewCommand(), 17 | } 18 | } 19 | 20 | func (cmd *Logout) parseFlag() error { 21 | var help bool 22 | 23 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 24 | fs.Usage = cmd.Usage 25 | 26 | fs.BoolVarP(&help, "help", "h", false, "help") 27 | 28 | if err := fs.Parse(os.Args[1:]); err != nil { 29 | fs.Usage() 30 | return err 31 | } 32 | 33 | if help { 34 | fs.Usage() 35 | return &ShowUsageError{} 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (cmd *Logout) Usage() { 42 | fmt.Println(`Usage: conoha logout [OPTIONS ...] 43 | 44 | DESCRIPTION 45 | Remove an authenticate file(~/.conoha-vps). 46 | 47 | OPTIONS 48 | -h: --help: Show usage. 49 | `) 50 | } 51 | 52 | func (cmd *Logout) Run() error { 53 | if err := cmd.parseFlag(); err != nil { 54 | return err 55 | } 56 | 57 | cmd.config.Remove() 58 | return nil 59 | } 60 | 61 | // Command構造体にあるShutdown()は、設定ファイルを作成してしまう。 62 | func (c *Logout) Shutdown() { 63 | log := lib.GetLogInstance() 64 | log.Debug("logout") 65 | } 66 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/hironobu-s/conoha-vps/cpanel" 5 | "github.com/hironobu-s/conoha-vps/lib" 6 | ) 7 | 8 | type ExitCode int 9 | 10 | const ( 11 | ExitCodeOK = iota 12 | ExitCodeNG 13 | ) 14 | 15 | type Commander interface { 16 | // コマンドライン引数を処理する 17 | parseFlag() error 18 | 19 | // // コマンドを実行する 20 | Run() error 21 | 22 | // コマンドのUsageを表示する 23 | Usage() 24 | 25 | // コマンド終了時の処理を記述する 26 | Shutdown() 27 | } 28 | 29 | type Command struct { 30 | config *lib.Config 31 | browser *cpanel.Browser 32 | } 33 | 34 | // Commandの実行が完了したときに呼ばれる関数。忘れずdeferすること。 35 | // ブラウザのセッションIDを設定ファイルに記録する。 36 | func (c *Command) Shutdown() { 37 | log := lib.GetLogInstance() 38 | 39 | c.config.Sid = c.browser.BrowserInfo.Sid() 40 | c.config.Write() 41 | 42 | log.Debug("write: " + c.browser.BrowserInfo.Sid()) 43 | } 44 | 45 | func NewCommand() *Command { 46 | // Configを作成 47 | c := &lib.Config{} 48 | c.Read() 49 | 50 | // ブラウザを作成してセッションIDをセットする 51 | browser := cpanel.NewBrowser() 52 | browser.BrowserInfo.FixSid(c.Sid) 53 | 54 | // コマンドを作成する 55 | cmd := &Command{ 56 | config: c, 57 | browser: browser, 58 | } 59 | return cmd 60 | } 61 | 62 | // USage()を表示するだけの場合でもErrorを返すことになるので、 63 | // この場合は専用のエラーを返すようにする。 64 | type ShowUsageError struct { 65 | s string 66 | } 67 | 68 | func (e ShowUsageError) Error() string { 69 | return e.s 70 | } 71 | -------------------------------------------------------------------------------- /lib/config_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | // テスト用のJSONデータ 11 | // 全部ダミーです 12 | var sampleConfigJson = `{ 13 | "Command": 0, 14 | "Token": "owe9f9jh93fe98u23jfi09824f9283f0", 15 | "TokenExpires": "Sat, 01 Jan 2015 08:18:34 UTC", 16 | "ApiUsername": "1111111", 17 | "ApiPassword": "testpassword", 18 | "TenantId": "oijewofijwoefinow0923f09jw30f9j0", 19 | "TenantId": "470710ce0ae24060886720fe4e7cf210", 20 | "TenantName": "1111111", 21 | "EndPointUrl": "https://objectstore-r1nd1001.cnode.jp/v1/oijewofijwoefinow0923f09jw30f9j0" 22 | }` 23 | 24 | func TestConfigFilePath(t *testing.T) { 25 | c := &Config{} 26 | 27 | path := c.ConfigFilePath() 28 | 29 | dir, file := filepath.Split(path) 30 | 31 | if fi, err := os.Stat(dir); fi == nil || err != nil { 32 | t.Errorf("wrong directory") 33 | } 34 | 35 | if file != CONFIGFILE { 36 | t.Errorf("file should be ConfigFile") 37 | } 38 | } 39 | 40 | func TestRead(t *testing.T) { 41 | c := &Config{} 42 | 43 | dir := os.TempDir() 44 | file := dir + CONFIGFILE 45 | 46 | if err := ioutil.WriteFile(file, []byte(sampleConfigJson), 0775); err != nil { 47 | t.Error(err) 48 | } 49 | 50 | err := c.Read() 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | } 55 | 56 | func TestWrite(t *testing.T) { 57 | c := &Config{} 58 | 59 | err := c.Write() 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/config.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/mitchellh/go-homedir" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | var Version string 11 | 12 | const ( 13 | CONFIGFILE = ".conoha-vps" 14 | ) 15 | 16 | type Config struct { 17 | Account string 18 | Password string 19 | Sid string 20 | } 21 | 22 | func (c *Config) ConfigFilePath() (string, error) { 23 | homedir, err := homedir.Dir() 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | return homedir + string(filepath.Separator) + CONFIGFILE, nil 29 | } 30 | 31 | func (c *Config) Remove() { 32 | var err error 33 | 34 | path, _ := c.ConfigFilePath() 35 | 36 | if _, err = os.Stat(path); err != nil { 37 | // ファイルが存在しない場合は何もしない 38 | return 39 | } 40 | 41 | os.Remove(path) 42 | } 43 | 44 | func (c *Config) Read() { 45 | var err error 46 | 47 | path, _ := c.ConfigFilePath() 48 | 49 | if _, err = os.Stat(path); err != nil { 50 | // ファイルが存在しない場合は何もしない 51 | return 52 | } 53 | 54 | file, err := os.Open(path) 55 | if err != nil { 56 | // 設定ファイルを開けない場合は何もしない 57 | return 58 | } 59 | 60 | dec := json.NewDecoder(file) 61 | if err := dec.Decode(&c); err != nil { 62 | // 設定ファイルが正しくない 63 | return 64 | } 65 | } 66 | 67 | func (c *Config) Write() error { 68 | var err error 69 | path, err := c.ConfigFilePath() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | file, err := os.Create(path) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if err = os.Chmod(path, 0600); err != nil { 80 | return err 81 | } 82 | 83 | enc := json.NewEncoder(file) 84 | err = enc.Encode(c) 85 | if err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hironobu-s/conoha-vps/command" 5 | "github.com/hironobu-s/conoha-vps/lib" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | var err error 11 | 12 | log := lib.GetLogInstance() 13 | 14 | var cmd command.Commander 15 | var subcommand string = "" 16 | 17 | if len(os.Args) > 1 { 18 | subcommand = os.Args[1] 19 | } 20 | 21 | log.Debugf("starting %s subcommand...", subcommand) 22 | 23 | switch subcommand { 24 | case "login": 25 | cmd = command.NewLogin() 26 | case "stat": 27 | cmd = command.NewVpsStat() 28 | case "list": 29 | cmd = command.NewVpsList() 30 | case "add": 31 | cmd = command.NewVpsAdd() 32 | case "remove": 33 | cmd = command.NewVpsRemove() 34 | case "label": 35 | cmd = command.NewVpsLabel() 36 | case "power": 37 | cmd = command.NewVpsPower() 38 | case "ssh-key": 39 | cmd = command.NewSshKey() 40 | case "ssh": 41 | cmd = command.NewSsh() 42 | case "logout": 43 | cmd = command.NewLogout() 44 | case "version": 45 | cmd = command.NewVersion() 46 | default: 47 | cmd = command.NewNocommand() 48 | } 49 | defer cmd.Shutdown() 50 | 51 | // login, logout, version, nocommand以外はログインが必須 52 | _, nocommand := cmd.(*command.Nocommand) 53 | if subcommand != "login" && subcommand != "version" && subcommand != "logout" && !nocommand { 54 | l := command.NewLogin() 55 | 56 | loggedIn, _ := l.LoggedIn() 57 | if !loggedIn { 58 | log.Debugf("Session is timed out. try relogin...") 59 | 60 | // 再ログイン 61 | loggedIn, err = l.Relogin() 62 | if !loggedIn { 63 | log.Errorf("Session is timed out. Please log in.") 64 | return 65 | } 66 | } 67 | } 68 | 69 | if err = cmd.Run(); err != nil { 70 | // ShowUsageErrorの場合はUsage()を表示してるだけなのでログは表示しない 71 | _, ok := err.(*command.ShowUsageError) 72 | if !ok { 73 | log.Error(err) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /command/ssh.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/hironobu-s/conoha-vps/lib" 7 | flag "github.com/ogier/pflag" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | type Ssh struct { 14 | // SSH接続先のVM 15 | vmId string 16 | 17 | // SSHユーザ 18 | sshUser string 19 | 20 | // SSHコマンドに渡すオプション 21 | sshOptions []string 22 | 23 | *Vps 24 | } 25 | 26 | func NewSsh() *Ssh { 27 | return &Ssh{ 28 | sshUser: "root", 29 | Vps: NewVps(), 30 | } 31 | } 32 | 33 | func (cmd *Ssh) parseFlag() error { 34 | var help bool 35 | 36 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 37 | fs.Usage = cmd.Usage 38 | 39 | // pflagsはparse()すると設定していないフラグが全てエラーになってしまう。 40 | // 仕方ないので、ssh コマンドにオプションを渡せるようにするため、自前でパースする。 41 | options := []string{} 42 | for i := 2; i < len(os.Args); i++ { 43 | // 最初の引数は - で開始してない場合はVPS-IDとみなす 44 | if i == 2 && !strings.HasPrefix(os.Args[i], "-") { 45 | cmd.vmId = os.Args[i] 46 | continue 47 | } 48 | 49 | if os.Args[i] == "-h" { 50 | help = true 51 | } else if os.Args[i] == "-u" { 52 | cmd.sshUser = os.Args[i+1] 53 | i++ 54 | } else { 55 | options = append(options, os.Args[i]) 56 | } 57 | } 58 | 59 | if help { 60 | fs.Usage() 61 | return &ShowUsageError{} 62 | } 63 | 64 | if cmd.vmId == "" { 65 | vm, err := cmd.Vps.vpsSelectMenu() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // 接続先のVmのID 71 | cmd.vmId = vm.Id 72 | } 73 | 74 | // SSHオプション 75 | cmd.sshOptions = options 76 | 77 | return nil 78 | } 79 | 80 | func (cmd *Ssh) Usage() { 81 | fmt.Println(`Usage: conoha ssh [OPTIONS ...] 82 | 83 | DESCRIPTION 84 | Login to VPS via SSH. 85 | There needs to be installed SSH client and all of option parameters will be passed into SSH command. 86 | 87 | It may not work on Windows. 88 | 89 | (Optional)VPS-ID to get the stats. It may be confirmed by LIST subcommand. 90 | If not set, It will be selected from the list menu. 91 | 92 | OPTIONS 93 | -u: --user: SSH username. 94 | -h: --help: Show usage. 95 | `) 96 | } 97 | 98 | func (cmd *Ssh) Run() error { 99 | 100 | log := lib.GetLogInstance() 101 | 102 | var err error 103 | if err = cmd.parseFlag(); err != nil { 104 | return err 105 | } 106 | 107 | vpsList := NewVpsList() 108 | vm := vpsList.Vm(cmd.vmId) 109 | if vm == nil { 110 | msg := fmt.Sprintf("VPS not found(id=%s).", cmd.vmId) 111 | return errors.New(msg) 112 | } 113 | 114 | // Windwsプランの場合は何もしない 115 | if strings.Index(vm.Plan, "Windows") >= 0 { 116 | log.Infof("ID=%s. Windows plan is not supported ssh connect.", vm.Id) 117 | return nil 118 | } 119 | 120 | vpsStat := NewVpsStat() 121 | stat, err := vpsStat.Stat(vm.Id) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | cmd.Connect(stat.IPv4, cmd.sshUser, cmd.sshOptions) 127 | 128 | return nil 129 | } 130 | 131 | func (cmd *Ssh) Connect(host string, user string, args []string) { 132 | 133 | options := []string{ 134 | user + "@" + host, 135 | } 136 | 137 | options = append(options, args...) 138 | 139 | sshCmd := "ssh" 140 | 141 | ssh := exec.Command(sshCmd, options...) 142 | ssh.Stdin = os.Stdin 143 | ssh.Stdout = os.Stdout 144 | ssh.Stderr = os.Stderr 145 | ssh.Run() 146 | } 147 | -------------------------------------------------------------------------------- /command/vps.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func NewVps() *Vps { 11 | return &Vps{ 12 | Command: NewCommand(), 13 | } 14 | } 15 | 16 | type Vps struct { 17 | info *VpsAddInformation 18 | *Command 19 | } 20 | 21 | func (cmd *Vps) parseFlag() error { 22 | return nil 23 | } 24 | 25 | func (cmd *Vps) Run() error { 26 | return nil 27 | } 28 | 29 | // VPSのステータス 30 | type ServerStatus int 31 | 32 | const ( 33 | StatusRunning = 1 // 稼働中 34 | StatusOffline = 4 // 停止 35 | StatusInUse = 6 // 取得中 36 | StatusInFormulation = 8 // サービス準備中 37 | StatusNoinformation = 98 // 未取得 38 | StatusUnknown = 99 39 | ) 40 | 41 | func (s ServerStatus) String() string { 42 | switch s { 43 | case StatusRunning: 44 | return "Running" 45 | //return "稼働中" 46 | case StatusOffline: 47 | return "Offline" 48 | //return "停止" 49 | case StatusInUse: 50 | return "No status" 51 | //return "取得中" 52 | case StatusInFormulation: 53 | return "Preparing" 54 | //return "サービス準備中" 55 | case StatusNoinformation: 56 | return "-" 57 | //return "未取得" 58 | case StatusUnknown: 59 | fallthrough 60 | default: 61 | return "Unknown" 62 | //return "不明" 63 | } 64 | } 65 | 66 | // 単一VPSを表す構造体 67 | // ServiceStatusとServerStatusは別物であることに注意 68 | type Vm struct { 69 | Id string 70 | TrId string // VPS削除などに使うもう一つのID。アプリケーション内のみで使用する。 71 | ServerStatus ServerStatus 72 | Label string 73 | ServiceStatus string 74 | ServiceId string 75 | Plan string 76 | CreatedAt time.Time 77 | DeleteDate time.Time 78 | PaymentSpan string 79 | 80 | // 詳細情報 81 | NumCpuCore string 82 | Memory string 83 | Disk1Size string 84 | Disk2Size string 85 | IPv4 string 86 | IPv4netmask string 87 | IPv4gateway string 88 | IPv4dns1 string 89 | IPv4dns2 string 90 | IPv6 []string 91 | IPv6prefix string 92 | IPv6gateway string 93 | IPv6dns1 string 94 | IPv6dns2 string 95 | House string 96 | CommonServerId string 97 | SerialConsoleHost string 98 | IsoUploadHost string 99 | } 100 | 101 | // VPSを選択する 102 | func (cmd *Vps) vpsSelectMenu() (*Vm, error) { 103 | var err error 104 | 105 | // VPS一覧 106 | vpsList := NewVpsList() 107 | servers, err := vpsList.List(false) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | // VPSが一つの場合はそれを返す 113 | if len(servers) == 1 { 114 | var vm *Vm 115 | for _, vm = range servers { 116 | break 117 | } 118 | return vm, nil 119 | 120 | } else { 121 | var i int 122 | ids := map[int]string{} 123 | for i, vm := range servers { 124 | fmt.Printf("[%d] %s\n", i+1, vm.Label) 125 | ids[i] = vm.Id 126 | i++ 127 | } 128 | 129 | fmt.Printf("Please select VPS no. [1-%d]: ", len(servers)) 130 | 131 | var no string 132 | if _, err = fmt.Scanf("%s", &no); err != nil { 133 | return nil, err 134 | } 135 | 136 | i, err = strconv.Atoi(no) 137 | if err != nil { 138 | return nil, err 139 | 140 | } else if 1 <= i && i <= len(servers) { 141 | return servers[i-1], nil 142 | 143 | } else { 144 | return nil, errors.New("Invalid input(out of range).") 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /command/vps_label.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hironobu-s/conoha-vps/cpanel" 8 | "github.com/hironobu-s/conoha-vps/lib" 9 | flag "github.com/ogier/pflag" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | // VPSのラベルを変更する 17 | 18 | type VpsLabel struct { 19 | vmId string 20 | label string 21 | *Vps 22 | } 23 | 24 | func NewVpsLabel() *VpsLabel { 25 | return &VpsLabel{ 26 | Vps: NewVps(), 27 | } 28 | } 29 | 30 | func (cmd *VpsLabel) parseFlag() error { 31 | var help bool 32 | 33 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 34 | fs.Usage = cmd.Usage 35 | 36 | fs.BoolVarP(&help, "help", "h", false, "help") 37 | fs.StringVarP(&cmd.label, "label", "l", "", "Label") 38 | 39 | if err := fs.Parse(os.Args[1:]); err != nil { 40 | fs.Usage() 41 | return err 42 | } 43 | 44 | if help { 45 | fs.Usage() 46 | return &ShowUsageError{} 47 | } 48 | 49 | if cmd.label == "" { 50 | return errors.New("Not enough arguments.") 51 | } 52 | 53 | if len(cmd.label) > 20 { 54 | return errors.New("Label is too long(should be 20 characters or less). ") 55 | } 56 | 57 | // VPS-ID 58 | if len(fs.Args()) < 2 { 59 | // コマンドライン引数で指定されていない場合は、標準入力から受け付ける 60 | vm, err := cmd.Vps.vpsSelectMenu() 61 | if err != nil { 62 | return err 63 | } 64 | cmd.vmId = vm.Id 65 | 66 | } else { 67 | cmd.vmId = os.Args[2] 68 | } 69 | return nil 70 | } 71 | 72 | func (cmd *VpsLabel) Usage() { 73 | fmt.Println(`Usage: conoha label [OPTIONS ...] 74 | 75 | DESCRIPTION 76 | Change VPS label. 77 | 78 | (Optional) VPS-ID to get the stats. It may be confirmed by LIST subcommand. 79 | If not set, It will be selected from the list menu. 80 | 81 | OPTIONS 82 | -l: --label: name of label. 83 | -h: --help: Show usage. 84 | `) 85 | } 86 | 87 | func (cmd *VpsLabel) Run() error { 88 | var err error 89 | if err = cmd.parseFlag(); err != nil { 90 | return err 91 | } 92 | 93 | return cmd.Change(cmd.vmId, cmd.label) 94 | } 95 | 96 | func (cmd *VpsLabel) Change(vmId string, label string) error { 97 | var act *cpanel.Action 98 | act = &cpanel.Action{ 99 | Request: &labelChangeRequest{ 100 | vmId: vmId, 101 | label: label, 102 | }, 103 | Result: &labelChangeResult{}, 104 | } 105 | cmd.browser.AddAction(act) 106 | 107 | if err := cmd.browser.Run(); err != nil { 108 | return err 109 | } 110 | 111 | log := lib.GetLogInstance() 112 | log.Infof(`VPS Label was changed to "%s"`, cmd.label) 113 | 114 | return nil 115 | } 116 | 117 | type labelChangeRequest struct { 118 | vmId string 119 | label string 120 | } 121 | 122 | func (r *labelChangeRequest) NewRequest(values url.Values) (*http.Request, error) { 123 | values = url.Values{} 124 | values.Add("eid", r.vmId) 125 | values.Add("label", r.label) 126 | values.Add("type", "vm") // 固定値 127 | 128 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/ChangeLabel.aspx", strings.NewReader(values.Encode())) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 134 | return req, err 135 | } 136 | 137 | type labelChangeResult struct{} 138 | 139 | func (r *labelChangeResult) Populate(resp *http.Response, doc *goquery.Document) error { 140 | 141 | if resp.StatusCode != 200 { 142 | return errors.New(fmt.Sprintf("Server returned the errror status code(%d).", resp.StatusCode)) 143 | } 144 | 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /cpanel/browser.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "errors" 5 | "github.com/PuerkitoBio/goquery" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | // コントロールパネル上のアクション 13 | type Action struct { 14 | Request ActionRequester 15 | Result ActionResulter 16 | } 17 | 18 | // アクションへのリクエストを作成する 19 | type ActionRequester interface { 20 | // HTTPリクエストを作成する 21 | NewRequest(values url.Values) (*http.Request, error) 22 | } 23 | 24 | // アクションの結果を格納する 25 | // HTMLの場合とJSONの場合、二つのインターフェイスを用意してある。 26 | type ActionResulter interface { 27 | } 28 | 29 | type HtmlActionResulter interface { 30 | // HTTPレスポンスをパースして、結果オブジェクトを作成する 31 | Populate(resp *http.Response, doc *goquery.Document) error 32 | } 33 | 34 | type JsonActionResulter interface { 35 | // HTTPレスポンスをパースして、結果オブジェクトを作成する 36 | Populate(resp *http.Response) error 37 | } 38 | 39 | func (act *Action) Run(bi *BrowserInfo) (err error) { 40 | 41 | if act.Request == nil || act.Result == nil { 42 | return errors.New("Some Struct fields of cpanel.Action undefined.") 43 | } 44 | 45 | // リクエストを作成 46 | req, err := act.Request.NewRequest(bi.Values) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // HTTPヘッダをセット 52 | for key, value := range bi.headers { 53 | req.Header.Set(key, value) 54 | } 55 | 56 | // HTTPリクエスト実行 57 | cli := &http.Client{Jar: bi.cookiejar} 58 | resp, err := cli.Do(req) 59 | 60 | if err != nil { 61 | return err 62 | } 63 | defer resp.Body.Close() 64 | 65 | // dump, _ := httputil.DumpRequest(req, true) 66 | // println(string(dump)) 67 | 68 | // if req.URL.String() == "https://cp.conoha.jp/Service/VPS/" && req.Method == "POST" { 69 | // dump, _ = httputil.DumpResponse(resp, false) 70 | // println(string(dump)) 71 | // } 72 | 73 | switch r := act.Result.(type) { 74 | case HtmlActionResulter: 75 | var doc *goquery.Document 76 | doc, err = goquery.NewDocumentFromResponse(resp) 77 | if err != nil { 78 | return err 79 | } 80 | // hiddenパラメータを取得 81 | bi.Values = act.hiddenParams(doc) 82 | 83 | // パース結果を返す 84 | return r.Populate(resp, doc) 85 | 86 | case JsonActionResulter: 87 | return r.Populate(resp) 88 | 89 | default: 90 | return errors.New("Undefined Result type.") 91 | } 92 | } 93 | 94 | // BrowserInfoにHTMLフォームに共通する "__" で始まるhidden要素を抽出してバインドする 95 | func (act Action) hiddenParams(doc *goquery.Document) url.Values { 96 | 97 | values := url.Values{} 98 | 99 | inputs := doc.Find("INPUT[type=hidden]") 100 | for i := range inputs.Nodes { 101 | n := inputs.Eq(i) 102 | name, exists := n.Attr("name") 103 | if !exists || strings.Index(name, "__") != 0 { 104 | //if !exists { 105 | continue 106 | } 107 | 108 | value, _ := n.Attr("value") 109 | 110 | values.Add(name, value) 111 | } 112 | 113 | return values 114 | } 115 | 116 | const ( 117 | COOKIE_URL = "https://cp.conoha.jp/" 118 | SESSION_NAME = "ASP.NET_SessionId" 119 | ) 120 | 121 | // Browserの設定情報 122 | type BrowserInfo struct { 123 | // CookieJar 124 | cookiejar *cookiejar.Jar 125 | 126 | // ブラウザが送るHTTPヘッダ 127 | headers map[string]string 128 | 129 | // リクエストに付与されるURL/POSTパラメータ 130 | Values url.Values 131 | } 132 | 133 | func (b *BrowserInfo) InitializeDefault() { 134 | b.headers = map[string]string{ 135 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:34.0) Gecko/20100101 Firefox/34.0", 136 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 137 | //"Accept-Language": "ja,en-us;q=0.7,en;q=0.3", 138 | "Accept-Language": "en-US,en;q=0.8,ja;q=0.6", 139 | } 140 | b.Values = url.Values{} 141 | b.cookiejar, _ = cookiejar.New(nil) 142 | } 143 | 144 | func (b *BrowserInfo) cookieUrl() *url.URL { 145 | u, _ := url.Parse(COOKIE_URL) 146 | return u 147 | } 148 | 149 | func (b *BrowserInfo) Sid() string { 150 | url := b.cookieUrl() 151 | for _, cookie := range b.cookiejar.Cookies(url) { 152 | if cookie.Name == SESSION_NAME { 153 | return cookie.Value 154 | } 155 | 156 | } 157 | return "" 158 | } 159 | 160 | func (b *BrowserInfo) FixSid(sid string) { 161 | url := b.cookieUrl() 162 | 163 | cookie := &http.Cookie{ 164 | Name: SESSION_NAME, 165 | Value: sid, 166 | } 167 | 168 | b.cookiejar.SetCookies(url, []*http.Cookie{ 169 | cookie, 170 | }) 171 | } 172 | 173 | // Webブラウザの代わりにコントロールパネルへアクセスする 174 | type Browser struct { 175 | // BrowserInfo 176 | BrowserInfo *BrowserInfo 177 | 178 | // 実行するリクエストのスライス 179 | actions []*Action 180 | } 181 | 182 | var browserInstance *Browser 183 | 184 | func NewBrowser() *Browser { 185 | 186 | if browserInstance == nil { 187 | 188 | info := &BrowserInfo{} 189 | info.InitializeDefault() 190 | 191 | browserInstance = &Browser{ 192 | BrowserInfo: info, 193 | } 194 | } 195 | return browserInstance 196 | } 197 | 198 | // アクションを追加する 199 | func (b *Browser) AddAction(act *Action) { 200 | b.actions = append(b.actions, act) 201 | } 202 | 203 | // アクションをすべて削除する 204 | func (b *Browser) ClearAction() { 205 | b.actions = []*Action{} 206 | } 207 | 208 | func (b *Browser) Run() error { 209 | for _, act := range b.actions { 210 | 211 | err := act.Run(b.BrowserInfo) 212 | if err != nil { 213 | b.ClearAction() 214 | return err 215 | } 216 | } 217 | 218 | b.ClearAction() 219 | return nil 220 | } 221 | -------------------------------------------------------------------------------- /command/vps_power.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/hironobu-s/conoha-vps/cpanel" 7 | "github.com/hironobu-s/conoha-vps/lib" 8 | flag "github.com/ogier/pflag" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | //COMMAND 17 | const ( 18 | BOOT = "Boot" 19 | REBOOT = "Reboot" 20 | SHUTDOWN = "Shutdown" 21 | STOP = "Stop" 22 | ) 23 | 24 | type VpsPower struct { 25 | vmId string 26 | command string 27 | forceSend bool 28 | 29 | *Vps 30 | } 31 | 32 | func NewVpsPower() *VpsPower { 33 | return &VpsPower{ 34 | Vps: NewVps(), 35 | } 36 | } 37 | 38 | func (cmd *VpsPower) parseFlag() error { 39 | var help bool 40 | var command string 41 | 42 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 43 | fs.Usage = cmd.Usage 44 | 45 | fs.BoolVarP(&help, "help", "h", false, "help") 46 | fs.StringVarP(&command, "command", "c", "", "power command") 47 | fs.BoolVarP(&cmd.forceSend, "force-send", "f", false, "force send") 48 | 49 | if err := fs.Parse(os.Args[1:]); err != nil { 50 | return err 51 | } 52 | 53 | if help { 54 | fs.Usage() 55 | return &ShowUsageError{} 56 | } 57 | 58 | if command == "" { 59 | fs.Usage() 60 | return errors.New("Not enough arguments.") 61 | } 62 | 63 | switch command { 64 | case "boot": 65 | cmd.command = BOOT 66 | case "reboot": 67 | cmd.command = REBOOT 68 | case "shutdown": 69 | cmd.command = SHUTDOWN 70 | case "stop": 71 | cmd.command = STOP 72 | default: 73 | return errors.New(fmt.Sprintf(`Undefined command "%s".`, command)) 74 | } 75 | 76 | if len(fs.Args()) < 2 { 77 | // コマンドライン引数で指定されていない場合は、標準入力から受け付ける 78 | vm, err := cmd.Vps.vpsSelectMenu() 79 | if err != nil { 80 | return err 81 | } 82 | cmd.vmId = vm.Id 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (cmd *VpsPower) Usage() { 89 | fmt.Println(`Usage: conoha power [OPTIONS] 90 | 91 | DESCRIPTION 92 | Send power-command to VPS. 93 | 94 | (Optional)VPS-ID to get the stats. It may be confirmed by LIST subcommand. 95 | If not set, It will be selected with prompting for VPS list. 96 | 97 | OPTIONS 98 | -c: --command: Power command. It should be one of following. 99 | ("boot" "reboot" "shutdown" "stop") 100 | 101 | -f: --force-send: Attempt to send without prompting for confirmation. 102 | 103 | -h: --help: Show usage. 104 | `) 105 | } 106 | 107 | func (cmd *VpsPower) Run() error { 108 | if err := cmd.parseFlag(); err != nil { 109 | return err 110 | } 111 | 112 | return cmd.SendCommand(cmd.vmId, cmd.command) 113 | } 114 | 115 | // 電源の状態を変更するコマンドを送信する 116 | func (cmd *VpsPower) SendCommand(vmId string, command string) error { 117 | 118 | // 対象のVMを特定する 119 | vpsList := NewVpsList() 120 | vm := vpsList.Vm(vmId) 121 | if vm == nil { 122 | return errors.New(fmt.Sprintf("VPS not found(id=%s).", vmId)) 123 | } 124 | 125 | // VPSのステータスを取得する 126 | stat, _ := cmd.GetVMStatus(vmId) 127 | 128 | // BOOTコマンドは停止中のVPSにのみ送信できる 129 | if command == BOOT && stat != StatusOffline { 130 | return errors.New(fmt.Sprintf(`Could not send "%s" command. VPS is already running.`, command)) 131 | 132 | // それ以外のコマンドは稼働中のVPSにのみ送信できる 133 | } else if command != BOOT && stat != StatusRunning { 134 | return errors.New(fmt.Sprintf(`Could not send "%s" command. VPS might be offiline.`, command)) 135 | } 136 | 137 | // 確認ダイアログ 138 | if !cmd.forceSend { 139 | if !cmd.confirmation(vm, command) { 140 | return nil 141 | } 142 | } 143 | 144 | // コマンドを送信する 145 | var act *cpanel.Action 146 | var err error 147 | 148 | act = &cpanel.Action{ 149 | Request: &vpsPowerRequest{ 150 | vmId: vmId, 151 | command: command, 152 | }, 153 | Result: &vpsPowerResult{}, 154 | } 155 | 156 | cmd.browser.AddAction(act) 157 | 158 | if err = cmd.browser.Run(); err != nil { 159 | return err 160 | } 161 | 162 | log := lib.GetLogInstance() 163 | log.Infof(`"%s" command was sent to VPS(id=%s).`, command, vmId) 164 | 165 | return nil 166 | } 167 | 168 | // 確認ダイアログ 169 | func (cmd *VpsPower) confirmation(vm *Vm, command string) bool { 170 | 171 | fmt.Printf(`Send "%s" command to VPS(Label=%s). Are you sure?`, command, vm.Label) 172 | fmt.Println("") 173 | fmt.Print("[y/N]: ") 174 | 175 | var no string 176 | if _, err := fmt.Scanf("%s", &no); err != nil { 177 | return false 178 | } 179 | 180 | if no == "y" { 181 | return true 182 | } else { 183 | return false 184 | } 185 | } 186 | 187 | type vpsPowerRequest struct { 188 | vmId string 189 | command string 190 | } 191 | 192 | func (r *vpsPowerRequest) NewRequest(values url.Values) (*http.Request, error) { 193 | values = url.Values{} 194 | values.Add("command", r.command) 195 | values.Add("evid", r.vmId) 196 | values.Add("_", strconv.FormatInt(time.Now().Unix(), 10)) // unix epoch 197 | 198 | req, err := http.NewRequest("GET", "https://cp.conoha.jp/Service/VPS/Control/CommandSender.aspx?"+values.Encode(), nil) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | return req, nil 204 | } 205 | 206 | type vpsPowerResult struct { 207 | } 208 | 209 | func (r *vpsPowerResult) Populate(resp *http.Response) error { 210 | 211 | if resp.StatusCode != 200 { 212 | return errors.New(fmt.Sprintf("Server returned the errror status code(%d).", resp.StatusCode)) 213 | } 214 | 215 | return nil 216 | } 217 | -------------------------------------------------------------------------------- /command/ssh_key.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hironobu-s/conoha-vps/cpanel" 8 | "github.com/hironobu-s/conoha-vps/lib" 9 | flag "github.com/ogier/pflag" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | type PrivateKey string 20 | 21 | type SshKey struct { 22 | *Vps 23 | destPath string 24 | sshKeyNo int 25 | } 26 | 27 | func NewSshKey() *SshKey { 28 | return &SshKey{ 29 | Vps: NewVps(), 30 | } 31 | } 32 | 33 | func (cmd *SshKey) parseFlag() error { 34 | var help bool 35 | 36 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 37 | fs.Usage = cmd.Usage 38 | 39 | fs.BoolVarP(&help, "help", "h", false, "help") 40 | fs.StringVarP(&cmd.destPath, "path", "f", "", ``) 41 | fs.IntVarP(&cmd.sshKeyNo, "sshkey-no", "s", 1, ``) 42 | 43 | if err := fs.Parse(os.Args[1:]); err != nil { 44 | fs.Usage() 45 | return err 46 | } 47 | 48 | if help { 49 | fs.Usage() 50 | return &ShowUsageError{} 51 | } 52 | 53 | if cmd.destPath == "" { 54 | // デフォルト 55 | cmd.destPath = "conoha-" + cmd.config.Account + "-" + strconv.Itoa(cmd.sshKeyNo) + ".key" 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (cd *SshKey) Usage() { 62 | fmt.Println(`Usage: conoha ssh-key [OPTIONS] 63 | 64 | DESCRIPTION 65 | Download and store SSH Private key. 66 | 67 | 68 | OPTIONS 69 | -f: --file: Local filename the private key is stored. 70 | Default is "conoha-{AccountID}.key" 71 | 72 | -s: --sshkey-no: SSH Key number. Default is 1. 73 | If the number of keys one, It wil be ignored. 74 | 75 | -h: --help: Show usage. 76 | `) 77 | } 78 | 79 | func (cmd *SshKey) Run() error { 80 | log := lib.GetLogInstance() 81 | 82 | var err error 83 | if err = cmd.parseFlag(); err != nil { 84 | return err 85 | } 86 | 87 | err = cmd.DownloadSshKey(cmd.destPath) 88 | if err == nil { 89 | log.Infof(`Download is complete. A private key is stored in "%s".`, cmd.destPath) 90 | return nil 91 | } else { 92 | return err 93 | } 94 | } 95 | 96 | // SSH秘密鍵をダウンロードする 97 | func (cmd *SshKey) DownloadSshKey(destPath string) error { 98 | var err error 99 | destPath, err = filepath.Abs(destPath) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | file, err := os.Create(destPath) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | key, err := cmd.SshKey() 110 | if err != nil { 111 | return err 112 | } 113 | 114 | // パーミッションを0600にセットする 115 | os.Chmod(destPath, 0600) 116 | 117 | if _, err = file.WriteString(string(key)); err != nil { 118 | return err 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // SSH秘密鍵を取得する 125 | func (cmd *SshKey) SshKey() (PrivateKey, error) { 126 | var err error 127 | 128 | var act *cpanel.Action 129 | 130 | // 秘密鍵一覧ページを取得して鍵の一覧を取得する 131 | rt := &sshDownloadFormResult{ 132 | sshKeyNo: cmd.sshKeyNo, 133 | } 134 | act = &cpanel.Action{ 135 | Request: &sshDownloadFormRequest{}, 136 | Result: rt, 137 | } 138 | cmd.browser.AddAction(act) 139 | 140 | rtd := &sshDownloadKeyResult{} 141 | act = &cpanel.Action{ 142 | Request: &sshDownloadKeyRequest{ 143 | formResult: rt, 144 | }, 145 | Result: rtd, 146 | } 147 | cmd.browser.AddAction(act) 148 | 149 | if err = cmd.browser.Run(); err != nil { 150 | return "", err 151 | } 152 | return rtd.SshKey, nil 153 | } 154 | 155 | type sshDownloadFormRequest struct { 156 | } 157 | 158 | func (r *sshDownloadFormRequest) NewRequest(values url.Values) (*http.Request, error) { 159 | return http.NewRequest("GET", "https://cp.conoha.jp/Service/VPS/keyPair/", nil) 160 | } 161 | 162 | type sshDownloadFormResult struct { 163 | sshKeyNo int 164 | SshKeyName string 165 | } 166 | 167 | func (r *sshDownloadFormResult) Populate(resp *http.Response, doc *goquery.Document) error { 168 | var sel *goquery.Selection 169 | sel = doc.Find("#ContentPlaceHolder1_ContentPlaceHolder1_gridSSHKeyList .btnIconPrivateKeyDL02") 170 | 171 | i := 0 172 | for n := range sel.Nodes { 173 | if i != r.sshKeyNo-1 { 174 | i++ 175 | continue 176 | } 177 | 178 | node := sel.Eq(n) 179 | name, exists := node.Attr("name") 180 | if exists { 181 | r.SshKeyName = name 182 | } 183 | break 184 | } 185 | 186 | if r.SshKeyName == "" { 187 | return errors.New("SSH Key not found.") 188 | } 189 | 190 | return nil 191 | } 192 | 193 | type sshDownloadKeyRequest struct { 194 | formResult *sshDownloadFormResult 195 | } 196 | 197 | func (r *sshDownloadKeyRequest) NewRequest(values url.Values) (*http.Request, error) { 198 | values.Add(r.formResult.SshKeyName, "Private Key Download") 199 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$hfTargetKey", "") 200 | 201 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/VPS/keyPair/", strings.NewReader(values.Encode())) 202 | if err != nil { 203 | return nil, err 204 | } 205 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 206 | return req, nil 207 | } 208 | 209 | type sshDownloadKeyResult struct { 210 | SshKey PrivateKey 211 | } 212 | 213 | func (r *sshDownloadKeyResult) Populate(resp *http.Response) error { 214 | body, err := ioutil.ReadAll(resp.Body) 215 | if err != nil { 216 | return err 217 | } 218 | 219 | r.SshKey = PrivateKey(body) 220 | return nil 221 | } 222 | -------------------------------------------------------------------------------- /command/login.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hironobu-s/conoha-vps/cpanel" 8 | "github.com/hironobu-s/conoha-vps/lib" 9 | "github.com/howeyc/gopass" 10 | flag "github.com/ogier/pflag" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | func NewLogin() *Login { 18 | return &Login{ 19 | Command: NewCommand(), 20 | } 21 | } 22 | 23 | type Login struct { 24 | account string 25 | password string 26 | 27 | *Command 28 | } 29 | 30 | func (cmd *Login) parseFlag() error { 31 | var help bool 32 | 33 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 34 | fs.Usage = cmd.Usage 35 | 36 | fs.BoolVarP(&help, "help", "h", false, "help") 37 | fs.StringVarP(&cmd.account, "account", "a", "", "ConoHa Account") 38 | fs.StringVarP(&cmd.password, "password", "p", "", "ConoHa Password") 39 | 40 | if err := fs.Parse(os.Args[1:]); err != nil { 41 | fs.Usage() 42 | return err 43 | } 44 | 45 | if help { 46 | fs.Usage() 47 | return &ShowUsageError{} 48 | } 49 | 50 | if cmd.account == "" || cmd.password == "" { 51 | 52 | // コマンドライン引数で指定されていない場合は、標準入力から受け付ける 53 | if err := cmd.inputAccountInfo(); err != nil { 54 | return errors.New("Not enough arguments.") 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (cd *Login) Usage() { 62 | fmt.Println(`Usage: conoha login [OPTIONS] 63 | 64 | DESCRIPTION 65 | Authenticate an account. 66 | If account or password not set, you can input interactively. 67 | 68 | OPTIONS 69 | -a: --account: ConoHa Account. 70 | -p: --password: Password. 71 | -h: --help: Show usage. 72 | `) 73 | } 74 | 75 | func (cmd *Login) Run() error { 76 | log := lib.GetLogInstance() 77 | 78 | var err error 79 | if err = cmd.parseFlag(); err != nil { 80 | return err 81 | } 82 | 83 | cmd.config.Account = cmd.account 84 | cmd.config.Password = cmd.password 85 | 86 | loggedIn, err := cmd.Login() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if loggedIn { 92 | log.Infof("Login Successfully.") 93 | } else { 94 | log.Infof("Login failed. Enter correct ConoHa account ID and password.") 95 | } 96 | return nil 97 | } 98 | 99 | func (cmd *Login) Relogin() (loggedIn bool, err error) { 100 | cmd.account = cmd.config.Account 101 | cmd.password = cmd.config.Password 102 | 103 | if cmd.account == "" || cmd.password == "" { 104 | return false, nil 105 | } 106 | 107 | return cmd.Login() 108 | } 109 | 110 | // 認証を実行してログイン状態を返す 111 | func (cmd *Login) Login() (loggedIn bool, err error) { 112 | var act *cpanel.Action 113 | 114 | act = &cpanel.Action{ 115 | Request: &loginFormRequest{}, 116 | Result: &loginFormResult{}, 117 | } 118 | cmd.browser.AddAction(act) 119 | 120 | act = &cpanel.Action{ 121 | Request: &loginDoRequest{ 122 | account: cmd.account, 123 | password: cmd.password, 124 | }, 125 | Result: &loginDoResult{}, 126 | } 127 | cmd.browser.AddAction(act) 128 | 129 | if err := cmd.browser.Run(); err != nil { 130 | return false, err 131 | } 132 | 133 | return cmd.LoggedIn() 134 | } 135 | 136 | // 標準入力からアカウントとパスワードを読み込む 137 | func (cmd *Login) inputAccountInfo() error { 138 | var n int 139 | var err error 140 | 141 | println("Please input ConoHa account.") 142 | print("ConoHa Account: ") 143 | n, err = fmt.Scanf("%s", &cmd.account) 144 | if n != 1 || err != nil { 145 | return err 146 | } 147 | 148 | print("Password: ") 149 | cmd.password = string(gopass.GetPasswd()) 150 | 151 | return nil 152 | } 153 | 154 | type loginFormRequest struct { 155 | } 156 | 157 | func (r *loginFormRequest) NewRequest(values url.Values) (*http.Request, error) { 158 | return http.NewRequest("GET", "https://cp.conoha.jp/Login.aspx", nil) 159 | } 160 | 161 | type loginFormResult struct { 162 | } 163 | 164 | func (r *loginFormResult) Populate(resp *http.Response, doc *goquery.Document) error { 165 | return nil 166 | } 167 | 168 | // --------------------- 169 | 170 | type loginDoRequest struct { 171 | account string 172 | password string 173 | } 174 | 175 | func (r *loginDoRequest) NewRequest(values url.Values) (req *http.Request, err error) { 176 | 177 | values.Set("ctl00$ContentPlaceHolder1$txtConoHaLoginID", r.account) 178 | values.Set("ctl00$ContentPlaceHolder1$txtConoHaLoginPW", r.password) 179 | values.Set("ctl00$ContentPlaceHolder1$btnLogin", "ログイン") 180 | 181 | req, err = http.NewRequest("POST", "https://cp.conoha.jp/Login.aspx", strings.NewReader(values.Encode())) 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 187 | req.Header.Set("Referer", "https://cp.conoha.jp/Login.aspx") 188 | 189 | return req, nil 190 | } 191 | 192 | type loginDoResult struct { 193 | } 194 | 195 | func (r *loginDoResult) Populate(resp *http.Response, doc *goquery.Document) error { 196 | return nil 197 | } 198 | 199 | // ログイン状態を返す。ログインしていればtrue していなければfalseが返る。 200 | // トップページを取得して、ヘッダー部にアカウントが含まれているかをチェックする 201 | func (cmd *Login) LoggedIn() (loggedIn bool, err error) { 202 | 203 | r := &loggedInResult{} 204 | act := &cpanel.Action{ 205 | Request: &loggedInRequest{}, 206 | Result: r, 207 | } 208 | 209 | cmd.browser.AddAction(act) 210 | if err := cmd.browser.Run(); err != nil { 211 | return false, err 212 | } else { 213 | return r.LoggedIn, nil 214 | } 215 | } 216 | 217 | type loggedInRequest struct { 218 | } 219 | 220 | func (r *loggedInRequest) NewRequest(values url.Values) (*http.Request, error) { 221 | return http.NewRequest("GET", "https://cp.conoha.jp/", nil) 222 | } 223 | 224 | type loggedInResult struct { 225 | LoggedIn bool 226 | } 227 | 228 | func (r *loggedInResult) Populate(resp *http.Response, doc *goquery.Document) error { 229 | accountId := doc.Find("#divLoginUser").Text() 230 | 231 | if accountId != "" { 232 | r.LoggedIn = true 233 | } else { 234 | r.LoggedIn = false 235 | } 236 | return nil 237 | } 238 | -------------------------------------------------------------------------------- /command/vps_remove.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // VPSを削除する 4 | // https://cp.conoha.jp/Service/VPS/Del/* のスクレイパー 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "github.com/PuerkitoBio/goquery" 10 | "github.com/hironobu-s/conoha-vps/cpanel" 11 | "github.com/hironobu-s/conoha-vps/lib" 12 | flag "github.com/ogier/pflag" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strings" 17 | ) 18 | 19 | type VpsRemove struct { 20 | vmId string 21 | forceRemove bool 22 | *Vps 23 | } 24 | 25 | func NewVpsRemove() *VpsRemove { 26 | return &VpsRemove{ 27 | Vps: NewVps(), 28 | } 29 | } 30 | 31 | func (cmd *VpsRemove) parseFlag() error { 32 | var help bool 33 | 34 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 35 | fs.Usage = cmd.Usage 36 | 37 | fs.BoolVarP(&help, "help", "h", false, "help") 38 | fs.BoolVarP(&cmd.forceRemove, "force-remove", "f", false, "force remove.") 39 | 40 | if err := fs.Parse(os.Args[1:]); err != nil { 41 | fs.Usage() 42 | return err 43 | } 44 | 45 | if help { 46 | fs.Usage() 47 | return &ShowUsageError{} 48 | } 49 | 50 | if len(fs.Args()) < 2 { 51 | // コマンドライン引数で指定されていない場合は、標準入力から受け付ける 52 | vm, err := cmd.Vps.vpsSelectMenu() 53 | if err != nil { 54 | return err 55 | } 56 | cmd.vmId = vm.Id 57 | 58 | } else { 59 | cmd.vmId = os.Args[2] 60 | } 61 | return nil 62 | } 63 | 64 | func (cd *VpsRemove) Usage() { 65 | fmt.Println(`Usage: conoha stat [OPTIONS] 66 | 67 | DESCRIPTION 68 | Remove VPS. 69 | 70 | (Optional)VPS-ID to get the stats. It may be confirmed by LIST subcommand. 71 | If not set, It will be selected with prompting for VPS list. 72 | 73 | OPTIONS 74 | -h: --help: Show usage. 75 | -f: --force-remove: Attempt to remove the VPS without prompting for confirmation. 76 | `) 77 | } 78 | 79 | func (cmd *VpsRemove) Run() error { 80 | var err error 81 | if err = cmd.parseFlag(); err != nil { 82 | return err 83 | } 84 | 85 | err = cmd.Remove(cmd.vmId) 86 | if err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | func (cmd *VpsRemove) Remove(vmId string) error { 93 | 94 | log := lib.GetLogInstance() 95 | 96 | // 削除対象のVMを特定する 97 | vpsList := NewVpsList() 98 | vm := vpsList.Vm(vmId) 99 | if vm == nil { 100 | msg := fmt.Sprintf("VPS not found(id=%s).", vmId) 101 | return errors.New(msg) 102 | } 103 | 104 | // 削除確認 105 | if !cmd.forceRemove { 106 | if !cmd.confirmationRemove(vm) { 107 | return nil 108 | } 109 | } 110 | 111 | // 削除実行 112 | var act *cpanel.Action 113 | act = &cpanel.Action{ 114 | Request: &removeFormRequest{ 115 | vm: vm, 116 | }, 117 | Result: &removeFormResult{}, 118 | } 119 | cmd.browser.AddAction(act) 120 | 121 | act = &cpanel.Action{ 122 | Request: &removeConfirmRequest{}, 123 | Result: &removeConfirmResult{}, 124 | } 125 | cmd.browser.AddAction(act) 126 | 127 | act = &cpanel.Action{ 128 | Request: &removeSubmitRequest{}, 129 | Result: &removeSubmitResult{}, 130 | } 131 | cmd.browser.AddAction(act) 132 | 133 | if err := cmd.browser.Run(); err != nil { 134 | return err 135 | } 136 | 137 | log.Infof("Removing VPS is complete.") 138 | 139 | return nil 140 | } 141 | 142 | // 削除確認ダイアログ 143 | func (cmd *VpsRemove) confirmationRemove(vm *Vm) bool { 144 | 145 | fmt.Printf("Remove VPS[Label=%s]. Are you sure?\n", vm.Label) 146 | fmt.Print("[y/N]: ") 147 | 148 | var no string 149 | if _, err := fmt.Scanf("%s", &no); err != nil { 150 | return false 151 | } 152 | 153 | if no == "y" { 154 | return true 155 | } else { 156 | return false 157 | } 158 | } 159 | 160 | type removeFormRequest struct { 161 | vm *Vm 162 | } 163 | 164 | func (r *removeFormRequest) NewRequest(values url.Values) (*http.Request, error) { 165 | // VPSのIDを指定 166 | values.Set("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$gridServiceList$"+r.vm.TrId+"$ctl01", "on") 167 | 168 | // これが削除ページのトリガになっているらしい 169 | values.Set("__EVENTTARGET", "ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$btnDel") 170 | 171 | // フォームを取得 172 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/VPS/", strings.NewReader(values.Encode())) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 178 | req.Header.Set("Referer", "https://cp.conoha.jp/Service/VPS/") 179 | 180 | return req, nil 181 | } 182 | 183 | type removeFormResult struct{} 184 | 185 | func (r *removeFormResult) Populate(resp *http.Response, doc *goquery.Document) error { 186 | 187 | // b, _ := ioutil.ReadAll(resp.Body) 188 | // fmt.Println("body: " + string(b)) 189 | 190 | // 確認ボタンが表示されていることを確認 191 | sel := doc.Find("#ContentPlaceHolder1_ContentPlaceHolder1_btnConfirm") 192 | v, _ := sel.Attr("value") 193 | if v == "" { 194 | return errors.New("Server returned the invalid body(Confirm button is not included).") 195 | } 196 | return nil 197 | } 198 | 199 | // --------------------------- 200 | 201 | type removeConfirmRequest struct{} 202 | 203 | func (r *removeConfirmRequest) NewRequest(values url.Values) (*http.Request, error) { 204 | values.Set("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$btnConfirm", "確認") 205 | 206 | // フォームを取得 207 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/VPS/Del/Default.aspx", strings.NewReader(values.Encode())) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 213 | 214 | return req, nil 215 | } 216 | 217 | type removeConfirmResult struct{} 218 | 219 | func (r *removeConfirmResult) Populate(resp *http.Response, doc *goquery.Document) error { 220 | // 決定ボタンが表示されていることを確認 221 | sel := doc.Find("#ContentPlaceHolder1_ContentPlaceHolder1_btnConfirm") 222 | v, _ := sel.Attr("value") 223 | if v == "" { 224 | return errors.New("Server returned the invalid body(Submit button is not included).") 225 | } 226 | return nil 227 | } 228 | 229 | // --------------------------- 230 | 231 | type removeSubmitRequest struct{} 232 | 233 | func (r *removeSubmitRequest) NewRequest(values url.Values) (*http.Request, error) { 234 | values.Set("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$btnConfirm", "決定") 235 | 236 | // フォームを取得 237 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/VPS/Del/Confirm.aspx", strings.NewReader(values.Encode())) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 243 | req.Header.Set("Referer", "https://cp.conoha.jp/Service/VPS/Del/Default.aspx") 244 | return req, nil 245 | } 246 | 247 | type removeSubmitResult struct{} 248 | 249 | func (r *removeSubmitResult) Populate(resp *http.Response, doc *goquery.Document) error { 250 | // 削除に成功するとBodyに通知メッセージが含まれている 251 | sel := doc.Find("#ltInfoMessage") 252 | if sel.Text() != "" { 253 | return nil 254 | } else { 255 | msg := fmt.Sprintf("Server returned the invalid body(Info Message is not include).") 256 | return errors.New(msg) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /command/vps_list.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // VPSの一覧を取得する 4 | // https://cp.conoha.jp/Service/VPS/ のスクレイパー 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "github.com/PuerkitoBio/goquery" 11 | "github.com/hironobu-s/conoha-vps/cpanel" 12 | flag "github.com/ogier/pflag" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | type VpsList struct { 23 | *Vps 24 | idOnly bool 25 | verbose bool 26 | } 27 | 28 | func NewVpsList() *VpsList { 29 | return &VpsList{ 30 | Vps: NewVps(), 31 | } 32 | } 33 | 34 | func (cmd *VpsList) parseFlag() error { 35 | var help bool 36 | 37 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 38 | fs.Usage = cmd.Usage 39 | 40 | fs.BoolVarP(&help, "help", "h", false, "help") 41 | fs.BoolVarP(&cmd.idOnly, "id-only", "i", false, "id-only") 42 | fs.BoolVarP(&cmd.verbose, "Verbose", "v", true, "Verbose output.") 43 | 44 | if err := fs.Parse(os.Args[1:]); err != nil { 45 | fs.Usage() 46 | return err 47 | } 48 | 49 | if help { 50 | fs.Usage() 51 | return &ShowUsageError{} 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (cd *VpsList) Usage() { 58 | fmt.Println(`Usage: conoha list [OPTIONS] 59 | 60 | DESCRIPTION 61 | List VPS status. 62 | 63 | OPTIONS 64 | -h: --help: Show usage. 65 | -i: --id-only: Show VPS-ID only. 66 | -v: --verbose: Verbose output(default is true). 67 | It will be included the server status, but slowly. 68 | `) 69 | } 70 | 71 | func (cmd *VpsList) Run() error { 72 | var err error 73 | 74 | if err = cmd.parseFlag(); err != nil { 75 | return err 76 | } 77 | 78 | var servers []*Vm 79 | servers, err = cmd.List(cmd.verbose) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if cmd.idOnly { 85 | format := "%-20s\n" 86 | for _, vm := range servers { 87 | fmt.Printf(format, vm.Id) 88 | } 89 | 90 | } else { 91 | 92 | var maxPlan int = 10 93 | var maxLabel int = 10 94 | for _, vm := range servers { 95 | if len(vm.Label) > maxLabel { 96 | maxLabel = len(vm.Label) 97 | } 98 | if len(vm.Plan) > maxPlan { 99 | maxPlan = len(vm.Plan) 100 | } 101 | } 102 | 103 | format := "%-20s\t%-" + strconv.Itoa(maxLabel) + "s\t%-" + strconv.Itoa(maxPlan) + "s\t%-15s\t%-20s\t%s\n" 104 | 105 | fmt.Printf( 106 | format, 107 | "VPS ID ", 108 | "Label", 109 | "Plan", 110 | "Server Status", 111 | "Service Status", 112 | "CreatedAt", 113 | ) 114 | for _, vm := range servers { 115 | fmt.Printf( 116 | format, 117 | vm.Id, 118 | vm.Label, 119 | vm.Plan, 120 | vm.ServerStatus, 121 | vm.ServiceStatus, 122 | vm.CreatedAt.Format("2006/01/02 15:04 MST"), 123 | ) 124 | } 125 | } 126 | return nil 127 | } 128 | 129 | // Vmを取得する 130 | // 引数のIDのVmが見つかった場合はその構造体を、見つからない場合はnilを返す。 131 | func (cmd *VpsList) Vm(vmId string) *Vm { 132 | var target *Vm 133 | 134 | servers, err := cmd.List(false) 135 | if err != nil { 136 | return nil 137 | } 138 | 139 | for _, vps := range servers { 140 | if vps.Id == vmId { 141 | target = vps 142 | } 143 | } 144 | 145 | return target 146 | } 147 | 148 | // VPSの一覧を取得して、IDをキー、Vm構造体のポインタを値としたスライスを返す 149 | // 引数のdeepCrawlをtrueにすると、VMのステータスも取得する 150 | func (cmd *VpsList) List(deep bool) (servers []*Vm, err error) { 151 | 152 | var act *cpanel.Action 153 | 154 | r := &listResult{} 155 | 156 | act = &cpanel.Action{ 157 | Request: &listRequest{}, 158 | Result: r, 159 | } 160 | cmd.browser.AddAction(act) 161 | 162 | if err := cmd.browser.Run(); err != nil { 163 | return nil, err 164 | } 165 | 166 | // サーバーステータスを取得する 167 | if deep { 168 | wait := new(sync.WaitGroup) 169 | 170 | for _, vm := range r.servers { 171 | 172 | wait.Add(1) 173 | 174 | go func(vm *Vm) { 175 | vm.ServerStatus, err = cmd.GetVMStatus(vm.Id) 176 | if err != nil { 177 | vm.ServerStatus = StatusUnknown 178 | } 179 | wait.Done() 180 | }(vm) 181 | 182 | wait.Wait() 183 | } 184 | 185 | } else { 186 | for _, vm := range servers { 187 | vm.ServerStatus = StatusNoinformation 188 | } 189 | } 190 | 191 | return r.servers, nil 192 | } 193 | 194 | // VPS一覧を取得するリクエスト 195 | type listRequest struct { 196 | } 197 | 198 | func (r *listRequest) NewRequest(values url.Values) (*http.Request, error) { 199 | return http.NewRequest("GET", "https://cp.conoha.jp/Service/VPS/", nil) 200 | } 201 | 202 | type listResult struct { 203 | servers []*Vm 204 | } 205 | 206 | func (r *listResult) Populate(resp *http.Response, doc *goquery.Document) error { 207 | 208 | // VPSの一覧を取得する 209 | sel := doc.Find("#gridServiceList TR") 210 | 211 | servers := []*Vm{} 212 | for i := range sel.Nodes { 213 | tr := sel.Eq(i) 214 | tds := tr.Find("TD") 215 | 216 | if len(tds.Nodes) == 0 { 217 | continue 218 | } 219 | 220 | // Vm構造体を準備 221 | vm := &Vm{} 222 | 223 | // TrIDを取得 224 | trid, exists := tr.Attr("id") 225 | if !exists { 226 | return errors.New("TrID not exists") 227 | } 228 | vm.TrId = trid 229 | 230 | // VMの各要素を取得 231 | c := 0 232 | for j := range tds.Nodes { 233 | 234 | value := strings.Trim(tds.Eq(j).Text(), " \t\r\n") 235 | switch c { 236 | case 1: 237 | // GetVMStatus()で設定するのでここでは初期値を設定 238 | vm.ServerStatus = StatusNoinformation 239 | case 2: 240 | vm.Label = value 241 | 242 | // VPSのIDを取得 243 | href, exists := tds.Eq(j).Find("A").Attr("href") 244 | if exists { 245 | sp := strings.Split(href, "/") 246 | vm.Id = sp[2] 247 | } else { 248 | // VPSの作成待ちの場合はIDが存在しない場合がある 249 | vm.Id = "" 250 | } 251 | 252 | case 3: 253 | vm.ServiceStatus = value 254 | case 4: 255 | vm.ServiceId = value 256 | case 5: 257 | vm.Plan = value 258 | case 6: 259 | vm.CreatedAt, _ = time.Parse("Jan/02/2006 15:04 MST", value+" JST") 260 | case 7: 261 | vm.DeleteDate, _ = time.Parse("Jan/02/2006 15:04 MST", value+" JST") 262 | case 8: 263 | vm.PaymentSpan = value 264 | } 265 | 266 | c++ 267 | } 268 | 269 | servers = append(servers, vm) 270 | } 271 | 272 | r.servers = servers 273 | 274 | return nil 275 | } 276 | 277 | // -------------------------------- 278 | 279 | // コントロールパネルのAjaxリクエストと同等 280 | // サーバーのステータス定数を返す 281 | type GetVMStatusJson struct { 282 | StatusId string `json:"status_id"` 283 | StatusName string `json:"status_name"` 284 | StatusClass string `json:"status_class"` 285 | } 286 | 287 | type vmStatusResult struct { 288 | VmId string 289 | Status ServerStatus 290 | } 291 | 292 | func (cmd *Vps) GetVMStatus(id string) (status ServerStatus, err error) { 293 | 294 | if id == "" { 295 | return StatusUnknown, nil 296 | } 297 | 298 | r := &vmStatusResult{} 299 | f := &vmStatusRequest{} 300 | 301 | act := &cpanel.Action{ 302 | Request: f, 303 | Result: r, 304 | } 305 | 306 | values := url.Values{} 307 | values.Add("evid", id) 308 | cmd.browser.BrowserInfo.Values = values 309 | 310 | cmd.browser.AddAction(act) 311 | 312 | if err = cmd.browser.Run(); err != nil { 313 | return StatusUnknown, err 314 | } else { 315 | return r.Status, nil 316 | } 317 | } 318 | 319 | type vmStatusRequest struct { 320 | } 321 | 322 | func (r *vmStatusRequest) NewRequest(values url.Values) (*http.Request, error) { 323 | u, err := url.Parse("https://cp.conoha.jp/Service/VPS/GetVMStatus.aspx?" + values.Encode()) 324 | if err != nil { 325 | return nil, err 326 | } 327 | 328 | return http.NewRequest("GET", u.String(), nil) 329 | } 330 | 331 | func (r *vmStatusResult) Populate(resp *http.Response) error { 332 | 333 | j := &GetVMStatusJson{} 334 | decoder := json.NewDecoder(resp.Body) 335 | err := decoder.Decode(j) 336 | if err != nil { 337 | r.Status = StatusUnknown 338 | return err 339 | } 340 | 341 | switch j.StatusName { 342 | case "Running": 343 | r.Status = StatusRunning 344 | case "Offline": 345 | r.Status = StatusOffline 346 | case "In-use": 347 | r.Status = StatusInUse 348 | case "In-formulation": 349 | r.Status = StatusInFormulation 350 | default: 351 | r.Status = StatusUnknown 352 | } 353 | return nil 354 | } 355 | -------------------------------------------------------------------------------- /command/vps_stat.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hironobu-s/conoha-vps/cpanel" 8 | flag "github.com/ogier/pflag" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | type VpsStat struct { 19 | *Vps 20 | vmId string 21 | incIPv6 bool 22 | } 23 | 24 | func NewVpsStat() *VpsStat { 25 | return &VpsStat{ 26 | Vps: NewVps(), 27 | } 28 | } 29 | 30 | func (cmd *VpsStat) parseFlag() error { 31 | var help bool 32 | 33 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 34 | fs.Usage = cmd.Usage 35 | 36 | fs.BoolVarP(&help, "help", "h", false, "help") 37 | fs.BoolVarP(&cmd.incIPv6, "include-ipv6", "6", false, "Including IPv6 informations.") 38 | 39 | if err := fs.Parse(os.Args[1:]); err != nil { 40 | fs.Usage() 41 | return err 42 | } 43 | 44 | if help { 45 | fs.Usage() 46 | return &ShowUsageError{} 47 | } 48 | 49 | if len(fs.Args()) < 2 { 50 | // コマンドライン引数で指定されていない場合は、標準入力から受け付ける 51 | vm, err := cmd.Vps.vpsSelectMenu() 52 | if err != nil { 53 | return err 54 | } 55 | cmd.vmId = vm.Id 56 | 57 | } else { 58 | cmd.vmId = os.Args[2] 59 | } 60 | return nil 61 | } 62 | 63 | func (cd *VpsStat) Usage() { 64 | fmt.Println(`Usage: conoha stat [OPTIONS] 65 | 66 | DESCRIPTION 67 | Show VPS stats. 68 | 69 | (Optional)VPS-ID to get the stats. It may be confirmed by LIST subcommand. 70 | If not set, It will be selected from the list menu. 71 | 72 | OPTIONS 73 | -h: --help: Show usage. 74 | -6 --include-ipv6: Include IPv6 informations in output. 75 | `) 76 | } 77 | 78 | func (cmd *VpsStat) Run() error { 79 | var err error 80 | if err = cmd.parseFlag(); err != nil { 81 | return err 82 | } 83 | 84 | vm, err := cmd.Stat(cmd.vmId) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | var lines []string = []string{} 90 | 91 | padding := 20 92 | format := "%-" + strconv.Itoa(padding) + "s %s" 93 | 94 | lines = append(lines, fmt.Sprintf(format, "VPS ID", vm.Id)) 95 | lines = append(lines, fmt.Sprintf(format, "ServerStatus", vm.ServerStatus)) 96 | lines = append(lines, fmt.Sprintf(format, "Label", vm.Label)) 97 | lines = append(lines, fmt.Sprintf(format, "ServiceStatus", vm.ServiceStatus)) 98 | lines = append(lines, fmt.Sprintf(format, "Service ID", vm.ServiceId)) 99 | lines = append(lines, fmt.Sprintf(format, "Plan", vm.Plan)) 100 | lines = append(lines, fmt.Sprintf(format, "Created At", vm.CreatedAt.Format(time.RFC3339))) 101 | 102 | if !vm.DeleteDate.IsZero() { 103 | lines = append(lines, fmt.Sprintf(format, "Delete Date", vm.DeleteDate)) 104 | } else { 105 | lines = append(lines, fmt.Sprintf(format, "Delete Date", "-")) 106 | } 107 | 108 | lines = append(lines, fmt.Sprintf(format, "Payment Span", vm.PaymentSpan)) 109 | lines = append(lines, fmt.Sprintf(format, "CPU", vm.NumCpuCore)) 110 | lines = append(lines, fmt.Sprintf(format, "Memory", vm.Memory)) 111 | lines = append(lines, fmt.Sprintf(format, "Disk1", vm.Disk1Size)) 112 | lines = append(lines, fmt.Sprintf(format, "Disk2", vm.Disk2Size)) 113 | 114 | lines = append(lines, fmt.Sprintf(format, "IPv4 Address", vm.IPv4)) 115 | lines = append(lines, fmt.Sprintf(format, "IPv4 Netmask", vm.IPv4netmask)) 116 | lines = append(lines, fmt.Sprintf(format, "IPv4 Gateway", vm.IPv4gateway)) 117 | lines = append(lines, fmt.Sprintf(format, "IPv4 DNS1", vm.IPv4dns1)) 118 | lines = append(lines, fmt.Sprintf(format, "IPv4 DNS2", vm.IPv4dns2)) 119 | 120 | if cmd.incIPv6 { 121 | for i := 0; i < len(vm.IPv6); i++ { 122 | if i == 0 { 123 | lines = append(lines, fmt.Sprintf(format, "IPv6 Address", vm.IPv6[i])) 124 | } else { 125 | lines = append(lines, fmt.Sprintf(format, "", vm.IPv6[i])) 126 | } 127 | } 128 | lines = append(lines, fmt.Sprintf(format, "IPv6 Gateway", vm.IPv6gateway)) 129 | lines = append(lines, fmt.Sprintf(format, "IPv6 DNS1", vm.IPv6dns1)) 130 | lines = append(lines, fmt.Sprintf(format, "IPv6 DNS2", vm.IPv6dns2)) 131 | } 132 | 133 | lines = append(lines, fmt.Sprintf(format, "Host Server", vm.House)) 134 | lines = append(lines, fmt.Sprintf(format, "Common Server ID", vm.CommonServerId)) 135 | lines = append(lines, fmt.Sprintf(format, "Serial Console(SSH)", vm.SerialConsoleHost)) 136 | lines = append(lines, fmt.Sprintf(format, "ISO Upload(SFTP)", vm.IsoUploadHost)) 137 | 138 | fmt.Println(strings.Join(lines, "\n")) 139 | 140 | return nil 141 | } 142 | 143 | // Vmの詳細を取得する 144 | func (cmd *VpsStat) Stat(vmId string) (*Vm, error) { 145 | vpsList := NewVpsList() 146 | vm := vpsList.Vm(vmId) 147 | if vm == nil { 148 | var msg string 149 | if vmId == "" { 150 | msg = fmt.Sprintf("VPS not found.") 151 | } else { 152 | msg = fmt.Sprintf("VPS not found(id=%s).", vmId) 153 | } 154 | return nil, errors.New(msg) 155 | } 156 | 157 | act := &cpanel.Action{ 158 | Request: &statRequest{ 159 | vm: vm, 160 | }, 161 | Result: &statResult{ 162 | vm: vm, 163 | }, 164 | } 165 | 166 | cmd.browser.AddAction(act) 167 | if err := cmd.browser.Run(); err != nil { 168 | return nil, err 169 | } 170 | 171 | status, err := cmd.GetVMStatus(vm.Id) 172 | if err != nil { 173 | return vm, err 174 | } 175 | vm.ServerStatus = status 176 | 177 | return vm, nil 178 | } 179 | 180 | type statRequest struct { 181 | vm *Vm 182 | } 183 | 184 | func (r *statRequest) NewRequest(values url.Values) (*http.Request, error) { 185 | rawurl := "https://cp.conoha.jp/Service/VPS/Control/Console/" + r.vm.Id 186 | u, err := url.Parse(rawurl) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | return http.NewRequest("GET", u.String(), nil) 192 | } 193 | 194 | type statResult struct { 195 | vm *Vm 196 | } 197 | 198 | func (r *statResult) Populate(resp *http.Response, doc *goquery.Document) error { 199 | subbox := doc.Find("#subCtrlBox .subCtrlList TD") 200 | 201 | // VPS詳細 202 | for i := range subbox.Nodes { 203 | td := subbox.Eq(i) 204 | 205 | // ラベルを削除 206 | spans := td.Find("SPAN") 207 | for j := range spans.Nodes { 208 | spans.Eq(j).Remove() 209 | } 210 | 211 | switch i { 212 | case 0: 213 | r.vm.NumCpuCore = td.Text() 214 | case 1: 215 | r.vm.Memory = td.Text() 216 | case 2: 217 | r.vm.Disk1Size = td.Text() 218 | case 3: 219 | r.vm.Disk2Size = td.Text() 220 | case 5: 221 | r.vm.IPv4 = td.Text() 222 | case 6: 223 | r.vm.IPv4netmask = td.Text() 224 | case 7: 225 | r.vm.IPv4gateway = td.Text() 226 | case 8: 227 | r.vm.IPv4dns1 = td.Text() 228 | case 9: 229 | r.vm.IPv4dns2 = td.Text() 230 | case 10: 231 | tmp := strings.Split(td.Text(), "\n") 232 | for i := 0; i < len(tmp); i++ { 233 | if ipv6 := strings.Trim(tmp[i], " \r\n\t"); ipv6 != "" { 234 | r.vm.IPv6 = append(r.vm.IPv6, ipv6) 235 | } 236 | } 237 | case 11: 238 | r.vm.IPv6prefix = td.Text() 239 | case 12: 240 | r.vm.IPv6gateway = td.Text() 241 | case 13: 242 | r.vm.IPv6dns1 = td.Text() 243 | case 14: 244 | r.vm.IPv6dns2 = td.Text() 245 | case 15: 246 | r.vm.House = td.Text() 247 | case 16: 248 | r.vm.CommonServerId = td.Text() 249 | } 250 | } 251 | 252 | // ------------------------ 253 | var err error 254 | if err = r.populateDate(doc); err != nil { 255 | return err 256 | } 257 | 258 | if err = r.populateUploadHosts(doc); err != nil { 259 | return err 260 | } 261 | return nil 262 | } 263 | 264 | func (r *statResult) populateDate(doc *goquery.Document) error { 265 | var body string 266 | var reg *regexp.Regexp 267 | var matches [][]string 268 | var err error 269 | var date time.Time 270 | 271 | // 利用開始日 272 | body = doc.Find("#subCtrlBoxNav .startData").Text() 273 | 274 | reg = regexp.MustCompile("Started:([0-9/]*)") 275 | matches = reg.FindAllStringSubmatch(body, -1) 276 | 277 | if len(matches) > 0 && len(matches[0]) > 1 && matches[0][1] != "" { 278 | date, err = time.Parse("2006/01/02 MST", matches[0][1]+" JST") 279 | if err != nil { 280 | return err 281 | } 282 | r.vm.CreatedAt = date 283 | } else if matches[0][1] == "" { 284 | // 日付が未定。何もしない 285 | } else { 286 | // パースエラー 287 | return errors.New("Parse error. Can't detect CreatedAt.") 288 | } 289 | 290 | // 削除予定日 291 | body = doc.Find("#subCtrlBoxNav .endData").Text() 292 | 293 | reg = regexp.MustCompile("Scheduled Removal Date:([0-9/]*)") 294 | matches = reg.FindAllStringSubmatch(body, -1) 295 | 296 | if len(matches) > 0 && len(matches[0]) > 1 && matches[0][1] != "" { 297 | date, err = time.Parse("2006/01/02 MST", matches[0][1]+" JST") 298 | if err == nil { 299 | r.vm.DeleteDate = date 300 | } 301 | } else if matches[0][1] == "" { 302 | // 日付が未定。何もしない 303 | } else { 304 | // パースエラー 305 | return errors.New("Parse error. Can't detect DeleteDate.") 306 | } 307 | return nil 308 | } 309 | 310 | func (r *statResult) populateUploadHosts(doc *goquery.Document) error { 311 | // ISOアップロード先とシリアルコンソール接続先 312 | body := doc.Find("DL.listStyle01").Text() 313 | reg := regexp.MustCompile("Connect to: (.+)/") 314 | 315 | matches := reg.FindAllStringSubmatch(body, -1) 316 | if len(matches) == 0 { 317 | // パースエラー 318 | return errors.New("Parse error. Can't detect ISO upload host or serial console host.") 319 | } 320 | 321 | for i := 0; i < len(matches); i++ { 322 | if strings.Index(matches[i][1], "console") >= 0 { 323 | r.vm.SerialConsoleHost = matches[i][1] 324 | } else if strings.Index(matches[i][1], "sftp") >= 0 { 325 | r.vm.IsoUploadHost = matches[i][1] 326 | } else { 327 | // パースエラー 328 | return errors.New("Parse error. Can't detect ISO upload host or serial console host.") 329 | } 330 | } 331 | return nil 332 | } 333 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CLI-tool for ConoHa VPS. 3 | 4 | [![Build Status](https://travis-ci.org/hironobu-s/conoha-vps.svg?branch=master)](https://travis-ci.org/hironobu-s/conoha-vps) 5 | 6 | ------ 7 | 8 | **ConoHa側の変更により、このツールはすでに使用できません** 9 | 10 | **This tools is obsolete and will not update.** 11 | 12 | ----- 13 | 14 | [ConoHa VPS](https://www.conoha.jp/)を操作するためのCLIツールです。 15 | 16 | ConoHa VPSのコントロールパネルは非常に使いやすく、VPSの操作に関して困ることはありません。 17 | ただAPIなどが用意されていないため、プログラムなどからVPSを操作することは難しいです。 18 | 19 | このツールを使うと、コマンドラインからConoHa VPSを操作することができます。 20 | シェルスクリプトやスクリプト言語から利用することで、VPSに対してある程度の自動化を行うことが可能です。 21 | 22 | ## 特長 23 | 24 | 現在対応しているのは以下の機能です。 25 | 26 | * ログイン/ログアウト 27 | * VPSの一覧取得 28 | * VPSの詳細を取得(名前、プラン、IPアドレス、収容先など) 29 | * VPSの作成 30 | * VPSの削除 31 | * SSH秘密鍵のダウンロード 32 | * VPSへのSSH接続 33 | * VPSの起動、再起動、シャットダウン、強制停止 34 | 35 | 36 | ## インストール 37 | 38 | ### MacOSX 39 | 40 | ターミナルなどから以下のコマンドを実行します。 41 | 42 | ```bash 43 | L=/usr/local/bin/conoha && curl -sL https://github.com/hironobu-s/conoha-vps/releases/download/v20150223.2/conoha-osx.amd64.gz | zcat > $L && chmod +x $L 44 | ``` 45 | 46 | アンインストールする場合は/usr/local/bin/conohaを削除してください。 47 | 48 | ### Linux 49 | 50 | ターミナルなどから以下のコマンドを実行します。/usr/local/binにインストールされるので、root権限が必要です。他のディレクトリにインストールする場合はL=/usr/local/bin/conohaの部分を適宜書き換えてください。 51 | 52 | ```bash 53 | L=/usr/local/bin/conoha && curl -sL https://github.com/hironobu-s/conoha-vps/releases/download/v20150223.2/conoha-linux.amd64.gz | zcat > $L && chmod +x $L 54 | ``` 55 | 56 | アンインストールする場合は/usr/local/bin/conohaを削除してください。 57 | 58 | ### Windows 59 | 60 | [ZIPファイル](https://github.com/hironobu-s/conoha-vps/releases/download/v20150223.2/conoha.amd64.zip)をダウンロードして、適当なフォルダに展開します。 61 | 62 | 実行する場合は、コマンドプロンプトから実行してください(ファイル名をダブルクリックしても何も起きません)。 63 | 64 | アンインストールする場合はファイルをゴミ箱に入れてください。 65 | 66 | 67 | ## クイックスタート 68 | 69 | 最初にログインします 70 | ``` 71 | $ conoha login 72 | ``` 73 | 74 | すると、ConoHaアカウントの入力プロンプトが出るので入力します。アカウントが正しいと「Login Successfully」となりログイン成功です。 75 | ``` 76 | $ conoha login 77 | Please input ConoHa accounts. 78 | ConoHa Account: [ACCOUNT] 79 | Password: [PASSWORD] 80 | INFO[0004] Login Successfully. 81 | ``` 82 | 83 | ログインに成功すると、全コマンドが実行できるようになります。コマンドの一覧は-hを付けると表示されます。 84 | ``` 85 | $ conoha -h 86 | Usage: conoha COMMAND [OPTIONS] 87 | 88 | DESCRIPTION 89 | A CLI-Tool for ConoHa VPS. 90 | 91 | COMMANDS 92 | add Add VPS. 93 | label Change VPS label. 94 | list List VPS. 95 | login Authenticate an account. 96 | logout Remove an authenticate file(~/.conoha-vps). 97 | power Send power-command to VPS. 98 | remove Remove VPS. 99 | ssh-key Download and store SSH Private key. 100 | ssh Login to VPS via SSH. 101 | stat Display VPS information. 102 | version Display version. 103 | ``` 104 | 105 | まずはlistコマンドを実行してみましょう。VPSの一覧が表示されます。 106 | ``` 107 | $conoha list 108 | VPS ID Label Plan Server Status Service Status CreatedAt 109 | f648a6646b7e7d91 CentOS7 8GB Memory Running In operation 2015/01/27 13:15 JST 110 | a2ae45355615d641 UbuntuDesktop 4GB Memory Offline In operation 2014/12/11 16:59 JST 111 | 28ff51fd97a96106 WindowsServer2012 8GB Memory - Windows Running In operation 2014/11/13 10:21 JST 112 | ``` 113 | 114 | 次にstatコマンドを実行してみましょう。メニューが表示され、選択したVPSの詳細情報が表示されます。(サンプルのため一部を***でマスクしています) 115 | ``` 116 | $ conoha stat 117 | [1] CentOS7 118 | [2] UbuntuDesktop 119 | [3] WindowsServer2012 120 | Please select VPS no. [1-3]: 1 121 | VPS ID f648a6646b7e7d91 122 | ServerStatus Running 123 | Label CentOS7 124 | ServiceStatus In operation 125 | Service ID VPS00708435 126 | Plan 8GB Memory 127 | Created At 2015-01-27T13:15:00+09:00 128 | Delete Date 0001-01-01 00:00:00 +0000 UTC 129 | Payment Span 1month 130 | CPU Virtual6Core 131 | Memory 8192MB 132 | Disk1 HDD 20GB 133 | Disk2 HDD 780GB 134 | IPv4 Address ***.***.***.*** 135 | IPv4 Netmask 255.255.254.0 136 | IPv4 Gateway ***.***.***.*** 137 | IPv4 DNS1 ***.***.***.*** 138 | IPv4 DNS2 ***.***.***.*** 139 | Host Server cnode-f0000 140 | Common Server ID iu3-0000000 141 | Serial Console(SSH) console1001.cnode.jp 142 | ISO Upload(SFTP) sftp1001.cnode.jp 143 | ``` 144 | 145 | このように、コマンドライン操作でVPSを操作することができます。 146 | 147 | 148 | ## コマンド一覧 149 | 150 | conohaコマンドがサポートしている機能の一覧です。 151 | 全てのコマンドに共通で、-hオプションを付けて実行すると、使い方を表示します。 152 | 153 | ### add 154 | 155 | 新しいVPSを追加します。以下のオプションを組み合わせることで、すべてのプラン種別(標準プラン=basic、Windowsプラン=windows)、プラン(1G, 2G, 4G, 8G, 16G)、テンプレートイメージ(CentOS, Nginx+WordPressなど)に対応します。 156 | 157 | [オプション] 158 | * -t, --type: VPS種別を指定します。"basic"か"windows"である必要があります。 159 | * -p: --plan: プランを指定します。1Gプランの場合は1,2Gプランの場合は2、と言うように指定します。 160 | * -P: --password: rootパスワードを指定します。標準プランのみです。 161 | * -i: --image: テンプレートイメージを指定します。"centos" "wordpress" "windows2012" "windows2008"のどれかである必要があります。 162 | * -s: --sshkey-no: ログインに使用するSSH秘密鍵を数値で指定します。SSH秘密鍵を複数作っている場合に有効です。デフォルトは1です。 163 | 164 | [コマンド実行例] 165 | 166 | 標準プラン1GBのVPSを追加する場合 167 | ``` 168 | $ conoha add -t basic -p 1 -i centos -P {password} 169 | ``` 170 | 171 | 標準プラン2GBのWordPressテンプレートを使ったVPSを追加する場合 172 | ``` 173 | $ conoha add -t basic -p 4 -i wordpress -P {password} 174 | ``` 175 | 176 | Windowsプランの8GBでWindows Server 2012を追加する場合 177 | ``` 178 | $ conoha add -t windows -p 8 -i windows2012 179 | ``` 180 | 181 | Windowsプランの16GBでWindows Server 2008を追加する場合 182 | ``` 183 | $ conoha add -t windows -p 16 -i windows2008 184 | ``` 185 | 186 | ### label 187 | 188 | VPSのラベルを変更します。 189 | ラベルは自由に付けることができ、VPSを識別するのに役立ちます。listコマンドやVPS選択のプロンプトでも表示されます。 190 | 191 | * -l, --label: ラベル名を指定します。このパラメータは必須です。 192 | 193 | ``` 194 | $ conoha label -l "Personal Web." 195 | [1] CentOS7 196 | [2] UbuntuDesktop 197 | [3] WindowsServer2012 198 | Please select VPS no. [1-3]: 1 199 | INFO[0006] VPS Label was changed to "Personal Web." 200 | $ 201 | ``` 202 | 203 | VPS IDを直接指定する場合は第二引数に指定します。 204 | 205 | ``` 206 | $ conoha label -l "Personal Web." 207 | ``` 208 | 209 | ### list 210 | 211 | VPSの一覧を表示します。 212 | 213 | * -v, --verbose: ServerStatusを取得します。デフォルトでOnですが実行に少し時間がかかります。 214 | * -i, --id-only: VPS-ID列のみを表示します。シェルスクリプトで使うときに便利です。 215 | 216 | ``` 217 | $ conoha list 218 | VPS ID Label Plan Server Status Service Status CreatedAt 219 | f648a6646b7e7d91 CentOS7 8GB Memory Running In operation 2015/01/27 13:15 JST 220 | a2ae45355615d641 UbuntuDesktop 4GB Memory Offline In operation 2014/12/11 16:59 JST 221 | 28ff51fd97a96106 WindowsServer2012 8GB Memory - Windows Running In operation 2014/11/13 10:21 JST 222 | ``` 223 | 224 | ### login 225 | 226 | ConoHaアカウントでログインします。versionなど一部のコマンドを除き、コマンドの実行にはログインが必須です。 227 | 実行するとアカウントとパスワードの入力プロンプトが表示されるので入力してください。 228 | 229 | アカウントとパスワードはオプション(-aと-p)で渡すこともできます。 230 | 231 | > **NOTE:** アカウントとパスワードなどをファイルに保持します。ファイルはホームディレクトリの.conoha-vpsで、パーミッションは0600です。 232 | 233 | ``` 234 | $ conoha login 235 | Please input ConoHa accounts. 236 | ConoHa Account: [ACCOUNT] 237 | Password: [PASSWORD] 238 | INFO[0004] Login Successfully. 239 | $ 240 | ``` 241 | 242 | 243 | ### logout 244 | 245 | ログアウトして認証ファイルを削除します。 246 | 247 | ``` 248 | $ conoha logout 249 | ``` 250 | 251 | 252 | ### power 253 | 254 | VPSの電源を操作するコマンドを送信します。つまり起動、再起動、シャットダウン、強制停止です。起動コマンドはVPSが停止していないと送信できません。また、他のコマンドはVPSが稼働していないと送信できません。 255 | 256 | [オプション] 257 | 258 | * -c, --command: 送信するコマンドを指定します。"boot" "reboot" "shutdown" "stop"のどれかを指定します。 259 | * -f, --force-send: 確認プロンプトを表示せず直ちにコマンドを送信します。 260 | 261 | ``` 262 | $ conoha power -c boot 263 | [1] CentOS7 264 | [2] Ubuntu Desktop 265 | [3] WindowsServer2012 266 | Please select VPS no. [1-3]: 1 267 | Send "Boot" command to VPS(Label=CentOS7). Are you sure? 268 | [y/N]: y 269 | INFO[0015] "Boot" command was sent to VPS(id=********). 270 | ``` 271 | 272 | 273 | ### remove 274 | 275 | VPSを削除します。実行すると、本当に削除するか確認ダイアログが表示され、Yesと回答すると削除が実行されます。 276 | 複数のVPSがある場合はVPSを選択するプロンプトが表示されますが、引数でVPS-IDを直接指定することもできます。 277 | 278 | [オプション] 279 | * -f, --force-remove: 確認プロンプトを表示せず直ちに削除を実行します。 280 | 281 | ``` 282 | $ conoha remove 283 | Remove VPS[Label=VPS00712702]. Are you sure? 284 | [y/N]: y 285 | INFO[0009] Removing VPS is complete. 286 | ``` 287 | 288 | 289 | ### ssh-key 290 | 291 | アカウントに紐付いたSSH秘密鍵を取得し保存します。 292 | 秘密鍵はconoha-[ACCOUNT].keyと言うファイル名で保存されますが、オプションでファイル名を指定することもできます。 293 | 294 | [オプション] 295 | * -f, --file: 保存する秘密鍵のファイル名を指定します 296 | * -s: --sshkey-no: ダウンロードするSSH秘密鍵を数値で指定します。SSH秘密鍵を複数作っている場合に有効です。デフォルトは1です。 297 | 298 | ``` 299 | $ conoha ssh-key 300 | INFO[0000] Download is complete. A private key is stored in "conoha-000000.key". 301 | ``` 302 | 303 | ### ssh 304 | 305 | SSHを使用してVPSに直接ログインします。 306 | 複数のVPSがある場合はVPSを選択するプロンプトが表示されますが、引数でVPS-IDを直接指定することもできます。 307 | 308 | このサブコマンドはsshコマンドのラッパーなので、sshがサポートしている機能は全て使えます。また渡されたオプションはサブコマンドのオプションでないものは全てsshコマンドにそのまま渡されます。これはつまり以下のような使い方ができると言うことです。 309 | 310 | ``` 311 | # -i オプションで秘密鍵ファイルを指定している。 312 | $ conoha ssh -i ~/.ssh/private.key 313 | ``` 314 | 315 | > **NOTE:** このサブコマンドはsshクライアントがインストールされていることが前提になります。またWindows環境では動作しません。 316 | 317 | [オプション] 318 | * -u: --user: SSH接続時のユーザ名を指定します。(デフォルトではrootを使用します) 319 | 320 | ``` 321 | $ conoha ssh 322 | Warning: Permanently added '***.***.***.***' (RSA) to the list of known hosts. 323 | Last login: Mon Feb 2 10:53:56 2015 from myhome.com 324 | [root@v***.***.***.*** ~]# 325 | ``` 326 | 327 | ### stat 328 | 329 | VPSの表債情報を表示します。 330 | 複数のVPSがある場合はVPSを選択するプロンプトが表示されますが、引数でVPS-IDを直接指定することもできます。 331 | 332 | オプションなしで実行すると、IPv6情報を表示しません。 333 | 334 | 335 | [オプション] 336 | 337 | * -6, --include-ipv6: 出力にIPv6情報を含めます 338 | 339 | ``` 340 | $ conoha stat 341 | (省略。出力サンプルについては「クイックスタート」をご覧ください) 342 | ``` 343 | 344 | ### version 345 | 346 | バージョンを表示します。 347 | 348 | ``` 349 | $ conoha version 350 | v20150203.4 351 | ``` 352 | 353 | ## ビルド方法 354 | 355 | 自分でビルドする場合は、以下の手順を参考にしてください。 356 | DebianやUbuntuはyumをaptにすれば大丈夫だと思います。 357 | 358 | ``` 359 | # Goの環境をインストール 360 | # また、依存ライブラリをgo getするのにMercurial(hg)が必要です。 361 | yum install go hg 362 | 363 | # GOPATHを設定 364 | export GOPATH=$HOME/go 365 | 366 | # ソースコードを取得 367 | go get github.com/hironobu-s/conoha-vps 368 | 369 | # ビルド 370 | make 371 | 372 | # 実行(binディレクトリ内にアーキテクチャ事にバイナリが作成されています 373 | ./bin/darwin/conoha 374 | 375 | ``` 376 | 377 | ## TODO 378 | 379 | * OS再インストールのサポート 380 | * statが遅い 381 | * ほかいろいろ 382 | 383 | ## License 384 | 385 | MIT License 386 | -------------------------------------------------------------------------------- /command/vps_add.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // VPSを追加する 4 | // https://cp.conoha.jp/Service/VPS/Add/ のスクレイパー 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "github.com/PuerkitoBio/goquery" 10 | "github.com/hironobu-s/conoha-vps/cpanel" 11 | "github.com/hironobu-s/conoha-vps/lib" 12 | flag "github.com/ogier/pflag" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strconv" 17 | "strings" 18 | ) 19 | 20 | const ( 21 | PlanTypeBasic = 1 + iota 22 | PlanTypeWindows 23 | ) 24 | 25 | const ( 26 | Plan1G = 1 + iota 27 | Plan2G 28 | Plan4G 29 | Plan8G 30 | Plan16G 31 | ) 32 | 33 | const ( 34 | TemplateDefault1 = 1 + iota 35 | TemplateDefault2 36 | TemplateDefault3 37 | TemplateDefault4 38 | ) 39 | 40 | // 追加するVPSの情報 41 | // VpsAdd.Add()に渡す場合は PlanType, Plan, Template, RootPasswordをセットすれば良い 42 | type VpsAddInformation struct { 43 | 44 | // プラン種別(PlanType*定数) 45 | PlanType int 46 | 47 | // プラン(Plan*定数) 48 | Plan int 49 | 50 | // テンプレートイメージ 51 | Template int 52 | 53 | // rootパスワード 54 | RootPassword string 55 | 56 | // SSHキーの番号 57 | SshKeyNo int 58 | 59 | // ---------- 60 | 61 | // VpsPlan構造体 62 | VpsPlan *VpsPlan 63 | 64 | // SSHキーID 65 | SshKeyId string 66 | } 67 | 68 | func (i *VpsAddInformation) Validate() error { 69 | if i.PlanType != PlanTypeBasic && i.PlanType != PlanTypeWindows { 70 | return errors.New("Invalid PlanType.") 71 | } 72 | 73 | if i.Plan != Plan1G && 74 | i.Plan != Plan1G && 75 | i.Plan != Plan2G && 76 | i.Plan != Plan4G && 77 | i.Plan != Plan8G && 78 | i.Plan != Plan16G { 79 | return errors.New("Invalid Plan.") 80 | } 81 | 82 | if i.Template != TemplateDefault1 && 83 | i.Template != TemplateDefault2 && 84 | i.Template != TemplateDefault3 && 85 | i.Template != TemplateDefault4 { 86 | return errors.New("Invalid Template.") 87 | } 88 | if i.PlanType == PlanTypeBasic && (i.Template == TemplateDefault3 || i.Template == TemplateDefault4) { 89 | return errors.New("Invalid Template.") 90 | } 91 | if i.PlanType == PlanTypeWindows && (i.Template == TemplateDefault1 || i.Template == TemplateDefault1) { 92 | return errors.New("Invalid Template.") 93 | } 94 | 95 | // 標準プランはrootパスワード必須 96 | if i.PlanType == PlanTypeBasic && i.RootPassword == "" { 97 | return errors.New("Root password is required.") 98 | } 99 | return nil 100 | } 101 | 102 | type VpsPlan struct { 103 | label string 104 | planId string 105 | } 106 | 107 | type VpsAdd struct { 108 | *Vps 109 | info *VpsAddInformation 110 | } 111 | 112 | func NewVpsAdd() *VpsAdd { 113 | return &VpsAdd{ 114 | Vps: NewVps(), 115 | info: &VpsAddInformation{}, 116 | } 117 | } 118 | 119 | func (cmd *VpsAdd) parseFlag() error { 120 | var help bool 121 | var plantype, template, root string 122 | var plan, sshKeyNo int 123 | var err error 124 | 125 | fs := flag.NewFlagSet("conoha-vps", flag.ContinueOnError) 126 | fs.Usage = cmd.Usage 127 | 128 | fs.BoolVarP(&help, "help", "h", false, "help") 129 | fs.StringVarP(&plantype, "type", "t", "", "") 130 | fs.IntVarP(&plan, "plan", "p", -1, "") 131 | fs.StringVarP(&template, "image", "i", "", "") 132 | fs.StringVarP(&root, "password", "P", "", "") 133 | fs.IntVarP(&sshKeyNo, "sshkey-no", "s", 1, ``) 134 | 135 | if err = fs.Parse(os.Args[1:]); err != nil { 136 | return err 137 | } 138 | 139 | // -------------- 140 | 141 | if help { 142 | fs.Usage() 143 | return &ShowUsageError{} 144 | } 145 | 146 | if plantype == "basic" { 147 | cmd.info.PlanType = PlanTypeBasic 148 | } else if plantype == "windows" { 149 | cmd.info.PlanType = PlanTypeWindows 150 | } else { 151 | fs.Usage() 152 | return errors.New(`PlanType(-t) parameter should be "basic" or "windows".`) 153 | } 154 | 155 | if plan == 1 { 156 | cmd.info.Plan = Plan1G 157 | } else if plan == 2 { 158 | cmd.info.Plan = Plan2G 159 | } else if plan == 4 { 160 | cmd.info.Plan = Plan4G 161 | } else if plan == 8 { 162 | cmd.info.Plan = Plan8G 163 | } else if plan == 16 { 164 | cmd.info.Plan = Plan16G 165 | } else { 166 | fs.Usage() 167 | return errors.New("Plan(-p) is invalid.") 168 | } 169 | 170 | if template == "centos" { 171 | cmd.info.Template = TemplateDefault1 172 | } else if template == "wordpress" { 173 | cmd.info.Template = TemplateDefault2 174 | } else if template == "windows2012" { 175 | cmd.info.Template = TemplateDefault3 176 | } else if template == "windows2008" { 177 | cmd.info.Template = TemplateDefault4 178 | } else { 179 | fs.Usage() 180 | return errors.New("Template Image(-i) is invalid.") 181 | } 182 | 183 | cmd.info.RootPassword = root 184 | cmd.info.SshKeyNo = sshKeyNo 185 | 186 | if err = cmd.info.Validate(); err != nil { 187 | fs.Usage() 188 | return err 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (cd *VpsAdd) Usage() { 195 | fmt.Println(`Usage: conoha add [OPTIONS] 196 | 197 | DESCRIPTION 198 | Add VPS to your account. 199 | 200 | OPTIONS 201 | -t, --type: VPS Type. 202 | Is should be "basic" or "windows". If not set, it will set "basic". 203 | 204 | -p: --plan: VPS Plan. 205 | It allow only numeric(1=1G, 2=2G ... 16=16G). 206 | 207 | -P: --password Root password. 208 | If the VPS Type is "basic" only. 209 | 210 | -i: --image: Template image. It should be one of the following. 211 | ("centos" "wordpress" "windows2012" "windows2008") 212 | 213 | -s: --sshkey-no: SSH Key number. Default is 1. 214 | If the number of keys one, It wil be ignored. 215 | 216 | EXAMPLE 217 | Standard Plan, 2vCPU, 1GB Memory and CentOS6.5. 218 | conoha add -t basic -p 1 -i centos -P {password} 219 | 220 | Standard Plan, 4vCPU, 4GB Memory and CentOS6.5 + nginx + WordPress. 221 | conoha add -t basic -p 4 -i wordpress -P {password} 222 | 223 | Windows Plan, 8vCPU, 8GB Memory and Windows Server 2012 R2 224 | conoha add -t windows -p 8 -i windows2012 225 | 226 | Windows Plan, 16vCPU, 16GB Memory and Windows Server 2008 R2 227 | conoha add -t windows -p 16 -i windows2008 228 | `) 229 | } 230 | 231 | func (cmd *VpsAdd) Run() error { 232 | var err error 233 | if err = cmd.parseFlag(); err != nil { 234 | return err 235 | } 236 | 237 | return cmd.Add(cmd.info) 238 | } 239 | 240 | func (cmd *Vps) Add(info *VpsAddInformation) error { 241 | 242 | log := lib.GetLogInstance() 243 | 244 | var act *cpanel.Action 245 | act = &cpanel.Action{ 246 | Request: &addFormRequest{}, 247 | Result: &addFormResult{ 248 | info: info, 249 | }, 250 | } 251 | cmd.browser.AddAction(act) 252 | 253 | act = &cpanel.Action{ 254 | Request: &addConfirmRequest{ 255 | info: info, 256 | }, 257 | Result: &addConfirmResult{}, 258 | } 259 | cmd.browser.AddAction(act) 260 | 261 | act = &cpanel.Action{ 262 | Request: &addSubmitRequest{}, 263 | Result: &addSubmitResult{}, 264 | } 265 | cmd.browser.AddAction(act) 266 | 267 | if err := cmd.browser.Run(); err != nil { 268 | return err 269 | } 270 | 271 | log.Infof("Adding VPS is complete.") 272 | 273 | return nil 274 | } 275 | 276 | // ---------------------- form -------------------- 277 | 278 | // フォームのHTMLを取得してパラメータを処理する 279 | type addFormRequest struct { 280 | } 281 | 282 | func (r *addFormRequest) NewRequest(values url.Values) (*http.Request, error) { 283 | // フォームを取得 284 | return http.NewRequest("GET", "https://cp.conoha.jp/Service/VPS/Add/", nil) 285 | } 286 | 287 | type addFormResult struct { 288 | info *VpsAddInformation 289 | } 290 | 291 | func (r *addFormResult) Populate(resp *http.Response, doc *goquery.Document) error { 292 | // プラン決定する 293 | plans, err := r.detectPlans(doc, r.info.PlanType) 294 | if err != nil { 295 | return err 296 | } 297 | r.info.VpsPlan = plans[r.info.Plan-1] 298 | 299 | // SSHキーIDを決定する 300 | r.info.SshKeyId, err = r.sshKeyId(doc) 301 | 302 | return err 303 | } 304 | 305 | // VPS追加フォームのHTMLからプラン一覧を作る 306 | // 返り値の1GB, 2GB, 4GB, 8GB, 16GBの5要素であることが保証されます。 307 | func (r *addFormResult) detectPlans(doc *goquery.Document, planType int) (plans []*VpsPlan, err error) { 308 | // Linux Plan 309 | plans = []*VpsPlan{} 310 | 311 | var sel *goquery.Selection 312 | if planType == PlanTypeBasic { 313 | sel = doc.Find("#trLinuxPlan LI") 314 | } else if planType == PlanTypeWindows { 315 | sel = doc.Find("#trWindowsPlan LI") 316 | } else { 317 | return nil, errors.New("Undefined plan type.") 318 | } 319 | 320 | i := 1 321 | for n := range sel.Nodes { 322 | node := sel.Eq(n) 323 | 324 | var planId, label string 325 | planId, _ = node.Find("INPUT").Attr("value") 326 | label = node.Text() 327 | 328 | // プラン名のメモリ容量をチェックする 329 | if strings.Index(label, strconv.Itoa(i)+"GB") < 0 { 330 | msg := fmt.Sprintf("Wrong plan name. [%s]", label) 331 | return nil, errors.New(msg) 332 | } 333 | 334 | p := &VpsPlan{ 335 | label: label, 336 | planId: planId, 337 | } 338 | plans = append(plans, p) 339 | 340 | i *= 2 341 | } 342 | 343 | if len(plans) != 5 { 344 | return nil, errors.New("The number of Linux plans is not 5.") 345 | } 346 | 347 | return plans, nil 348 | } 349 | 350 | // VPS追加フォームのHTMLからSSH公開鍵のIDを取得する 351 | func (r *addFormResult) sshKeyId(doc *goquery.Document) (string, error) { 352 | no := r.info.SshKeyNo - 1 353 | sshKeyId, _ := doc.Find("#ContentPlaceHolder1_ContentPlaceHolder1_rbKey_" + strconv.Itoa(no)).Attr("value") 354 | 355 | if sshKeyId != "" { 356 | return sshKeyId, nil 357 | } else { 358 | return "", errors.New("SSH key not found.") 359 | } 360 | } 361 | 362 | // ---------------------- confirm -------------------- 363 | 364 | // Confirmページのフォームを埋めてPOSTする 365 | type addConfirmRequest struct { 366 | info *VpsAddInformation 367 | } 368 | 369 | func (r *addConfirmRequest) NewRequest(values url.Values) (*http.Request, error) { 370 | 371 | info := r.info 372 | 373 | // プラン種別 374 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$rbPlanCategory", strconv.Itoa(info.PlanType)) 375 | 376 | // プラン 377 | if info.PlanType == PlanTypeBasic { 378 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$rbLinuxPlan", info.VpsPlan.planId) 379 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$rbWindowsPlan", "2364") 380 | } else { 381 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$rbLinuxPlan", "") 382 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$rbWindowsPlan", info.VpsPlan.planId) 383 | } 384 | 385 | // 支払い(固定値) 386 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$UnitMonth", "selectCredit1") 387 | 388 | // テンプレートイメージ 389 | if info.PlanType == PlanTypeBasic { 390 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$selLinuxOS", "default/"+strconv.Itoa(info.Template)) 391 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$selWindowsOS", "default/"+strconv.Itoa(TemplateDefault3)) 392 | } else { 393 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$selLinuxOS", "default/"+strconv.Itoa(TemplateDefault1)) 394 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$selWindowsOS", "default/"+strconv.Itoa(info.Template)) 395 | } 396 | 397 | // rootパスワード(標準プランのみ) 398 | if info.PlanType == PlanTypeBasic { 399 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$txtRootPassword", info.RootPassword) 400 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$txtConfirmPassword", info.RootPassword) 401 | 402 | } else { 403 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$txtRootPassword", "") 404 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$txtConfirmPassword", "") 405 | } 406 | 407 | // SSHキー 408 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$rbKey", info.SshKeyId) 409 | 410 | // ほか(適当な固定値でかまわない) 411 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$btnConfirm", "確認") 412 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$hfCpu", "2") 413 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$hfDisk", "100") 414 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$hfMemory", "1") 415 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$hfInital", "0円") 416 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$hfRunning", "507円") 417 | 418 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/VPS/Add/", strings.NewReader(values.Encode())) 419 | if err != nil { 420 | return nil, err 421 | } 422 | 423 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 424 | 425 | return req, nil 426 | 427 | } 428 | 429 | type addConfirmResult struct { 430 | } 431 | 432 | func (r addConfirmResult) Populate(resp *http.Response, doc *goquery.Document) error { 433 | var sel *goquery.Selection 434 | 435 | // rootパスワード不備などのフォームエラー 436 | sel = doc.Find(".errorMsg") 437 | for i := range sel.Nodes { 438 | node := sel.Eq(i) 439 | return errors.New(strings.Trim(node.Text(), "\r\n \t")) 440 | } 441 | 442 | // 追加ボタンが存在しない場合はエラー 443 | sel = doc.Find("#ContentPlaceHolder1_ContentPlaceHolder1_btnExecute") 444 | v, _ := sel.Attr("value") 445 | if v == "" { 446 | return errors.New("Server returned invalid html(Submit button is not included).") 447 | } 448 | 449 | return nil 450 | } 451 | 452 | // ---------------------- submit -------------------- 453 | 454 | type addSubmitRequest struct { 455 | } 456 | 457 | func (r *addSubmitRequest) NewRequest(values url.Values) (*http.Request, error) { 458 | values.Add("ctl00$ctl00$ContentPlaceHolder1$ContentPlaceHolder1$btnExecute", "決定") 459 | 460 | req, err := http.NewRequest("POST", "https://cp.conoha.jp/Service/VPS/Add/Confirm.aspx", strings.NewReader(values.Encode())) 461 | if err != nil { 462 | return nil, err 463 | } 464 | 465 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 466 | 467 | return req, nil 468 | } 469 | 470 | type addSubmitResult struct { 471 | } 472 | 473 | func (r *addSubmitResult) Populate(resp *http.Response, doc *goquery.Document) error { 474 | // 追加に成功するとBodyに通知メッセージが含まれている 475 | sel := doc.Find("#ltInfoMessage") 476 | if sel.Text() != "" { 477 | return nil 478 | } else { 479 | msg := fmt.Sprintf("Server returned the invalid body(Info Message is not include).", resp.StatusCode) 480 | return errors.New(msg) 481 | } 482 | } 483 | --------------------------------------------------------------------------------