Table Of Contents

  • Categories
  • diy

Cloudcone是2017年成立于美国的一家主机供应商。继vultr变得“不靠谱”和低价vm资源经常“售尽”的情况下,我转而使用cloudcone提供的vps服务用于日常学习和开发。

除了支持按小时计费、提供更换IP服务及支持支付宝/paypal等灵活的支付方式外,我最看重的是,它的价格能做到真正的弹性,即在vm关机状态不计费CPU/内存等费用,比online费用大概减半。

类似于其他云供应商平台,cloudcone还提供了开放的API来管理云主机

这里为了方便使用和合理控制资源,自己便用go写了一个小程序

代码实现

主要逻辑如下,通过cloudcone 提供的API。每天晚上23左右关机,次日早晨7点左右启动 如果调用失败的话,会再次重试一次。(如果依然不行的话,就当运气不好了:<)

以下是草稿版本代码

  • secret.go
package main

var (
    // 配置相关信息,这里我填写的是假的啦
	serverID  = 123456
	appSecret = "Q2aQr9323QE233r2"
	hashCode  = "zXqV22222222222222222222222222222222225bL"
)

serverID可以在compute菜单控制面板下查看,或查看浏览器url里的参数。

appSecrethashCode访问https://app.cloudcone.com/user/api创建即可。

  • main.go
/*
* cloudcone api https://api.cloudcone.com/
 */

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"sync"
	"time"

	"github.com/robfig/cron"
)

// CloudconeResponse represents the API Response
type CloudconeResponse struct {
	Status  int
	Message string
	Data    interface{} `json:"_data"`
}

const (
	baseAPIURL = "https://api.cloudcone.com/api/v1"
)

var (
	client = &http.Client{}
)

func main() {
	f, err := os.OpenFile("app.log", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0644)
	if err != nil {
		log.Fatalf("Error while opening file: %v\n", err)
	}
	defer f.Close()
	log.SetOutput(f)

	wg := sync.WaitGroup{}
	_, cancel := context.WithCancel(context.Background())

	c := cron.New()
	// boot at 07:00
	c.AddFunc("0 0 7 * * *", func() {
		log.Println("booting at time", time.Now())

		if err := boot(); err != nil {
			log.Println(err)
			time.Sleep(15 * time.Second)
			// try again
			boot()
		}
	})
	// shutdown at 23:45
	c.AddFunc("0 45 23 * * *", func() {
		log.Println("Shutdown at time", time.Now())

		if err := shutdown(); err != nil {
			log.Println(err)
			time.Sleep(15 * time.Second)
			// try again
			shutdown()
		}
	})
	c.Start()

	wg.Add(1)
	go func() {
		defer wg.Done()
		os.Stdin.Read(make([]byte, 1)) // wait for Enter keystroke
		c.Stop()
		cancel() // cancel the associated context
	}()

	log.Println("Running...")
	wg.Wait()
}

func boot() error {
	// compute/:id/boot
	endpoint := fmt.Sprintf("%s/compute/%d/%s", baseAPIURL, serverID, "boot")
	req, err := http.NewRequest("GET", endpoint, nil)
	if err != nil {
		return err
	}

	req.Header.Set("App-Secret", appSecret)
	req.Header.Set("Hash", hashCode)

	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	fmt.Println(string(body))

	var dat CloudconeResponse
	if err := json.Unmarshal(body, &dat); err != nil {
		return err
	}
	if dat.Status != 1 {
		return errors.New(dat.Message)
	}

	log.Println(dat.Message)
	return nil
}

func shutdown() error {
	// compute/:id/shutdown
	endpoint := fmt.Sprintf("%s/compute/%d/%s", baseAPIURL, serverID, "shutdown")
	req, err := http.NewRequest("GET", endpoint, nil)
	if err != nil {
		return err
	}

	req.Header.Set("App-Secret", appSecret)
	req.Header.Set("Hash", hashCode)

	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	var dat CloudconeResponse
	if err := json.Unmarshal(body, &dat); err != nil {
		return err
	}
	if dat.Status != 1 {
		return errors.New(dat.Message)
	}

	log.Println(dat.Message)
	return nil
}

参考