Initial version
This commit is contained in:
parent
54d2b32d8c
commit
af333a5de0
|
@ -0,0 +1,60 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
//revive:disable:var-naming
|
||||||
|
|
||||||
|
// Artifact represents a image as the result of a Packer build.
|
||||||
|
type Artifact struct {
|
||||||
|
image *Image
|
||||||
|
driver Driver
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuilderID returns the builder Id.
|
||||||
|
//revive:disable:var-naming
|
||||||
|
func (*Artifact) BuilderId() string {
|
||||||
|
return BuilderID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy destroys the image represented by the artifact.
|
||||||
|
func (a *Artifact) Destroy() error {
|
||||||
|
log.Printf("Destroying image: %s", a.image.Name)
|
||||||
|
errCh := a.driver.DeleteImage(a.image.Name)
|
||||||
|
return errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files returns the files represented by the artifact.
|
||||||
|
func (*Artifact) Files() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Id returns the image name.
|
||||||
|
//revive:disable:var-naming
|
||||||
|
func (a *Artifact) Id() string {
|
||||||
|
return a.image.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the artifact.
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
return fmt.Sprintf("A disk image was created: %v (id: %v)", a.image.Name, a.image.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
switch name {
|
||||||
|
case "ImageID":
|
||||||
|
return a.image.ID
|
||||||
|
case "ImageName":
|
||||||
|
return a.image.Name
|
||||||
|
case "ImageSizeGb":
|
||||||
|
return a.image.SizeGb
|
||||||
|
case "FolderID":
|
||||||
|
return a.config.FolderID
|
||||||
|
case "BuildZone":
|
||||||
|
return a.config.Zone
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ArtifactMini struct {
|
||||||
|
config *Config
|
||||||
|
imageID string
|
||||||
|
imageName string
|
||||||
|
imageFamily string
|
||||||
|
}
|
||||||
|
|
||||||
|
//revive:disable:var-naming
|
||||||
|
func (*ArtifactMini) BuilderId() string {
|
||||||
|
return BuilderID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtifactMini) Id() string {
|
||||||
|
return a.imageID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ArtifactMini) Files() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//revive:enable:var-naming
|
||||||
|
func (a *ArtifactMini) String() string {
|
||||||
|
return fmt.Sprintf("A disk image was created: %v (id: %v) (family: %v)", a.imageName, a.imageID, a.imageFamily)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArtifactMini) State(name string) interface{} {
|
||||||
|
switch name {
|
||||||
|
case "ImageID":
|
||||||
|
return a.imageID
|
||||||
|
case "FolderID":
|
||||||
|
return a.config.FolderID
|
||||||
|
case "BuildZone":
|
||||||
|
return a.config.Zone
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ArtifactMini) Destroy() error {
|
||||||
|
// no destroy right now
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/common"
|
||||||
|
"github.com/hashicorp/packer/helper/communicator"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The unique ID for this builder.
|
||||||
|
const BuilderID = "packer.yandex"
|
||||||
|
|
||||||
|
// Builder represents a Packer Builder.
|
||||||
|
type Builder struct {
|
||||||
|
config *Config
|
||||||
|
runner multistep.Runner
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare processes the build configuration parameters.
|
||||||
|
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
|
c, warnings, errs := NewConfig(raws...)
|
||||||
|
if errs != nil {
|
||||||
|
return warnings, errs
|
||||||
|
}
|
||||||
|
b.config = c
|
||||||
|
return warnings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes a yandex Packer build and returns a packer.Artifact
|
||||||
|
// representing a Yandex Cloud machine image.
|
||||||
|
func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||||
|
driver, err := NewDriverYandexCloud(ui, b.config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the state
|
||||||
|
state := &multistep.BasicStateBag{}
|
||||||
|
state.Put("config", b.config)
|
||||||
|
state.Put("driver", driver)
|
||||||
|
state.Put("sdk", driver.SDK())
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
|
||||||
|
// Build the steps
|
||||||
|
steps := []multistep.Step{
|
||||||
|
&stepCreateSSHKey{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
DebugKeyPath: fmt.Sprintf("yc_%s.pem", b.config.PackerBuildName),
|
||||||
|
},
|
||||||
|
&stepCreateInstance{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
SerialLogFile: b.config.SerialLogFile,
|
||||||
|
},
|
||||||
|
&stepInstanceInfo{},
|
||||||
|
&communicator.StepConnect{
|
||||||
|
Config: &b.config.Communicator,
|
||||||
|
Host: commHost,
|
||||||
|
SSHConfig: b.config.Communicator.SSHConfigFunc(),
|
||||||
|
},
|
||||||
|
&common.StepProvision{},
|
||||||
|
&common.StepCleanupTempKeys{
|
||||||
|
Comm: &b.config.Communicator,
|
||||||
|
},
|
||||||
|
&stepShutdown{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
},
|
||||||
|
&stepCreateImage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the steps
|
||||||
|
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||||
|
b.runner.Run(state)
|
||||||
|
|
||||||
|
// Report any errors
|
||||||
|
if rawErr, ok := state.GetOk("error"); ok {
|
||||||
|
return nil, rawErr.(error)
|
||||||
|
}
|
||||||
|
if _, ok := state.GetOk("image_id"); !ok {
|
||||||
|
log.Println("Failed to find image_id in state. Bug?")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact := &ArtifactMini{
|
||||||
|
imageID: state.Get("image_id").(string),
|
||||||
|
imageName: state.Get("image_name").(string),
|
||||||
|
imageFamily: state.Get("image_family").(string),
|
||||||
|
config: b.config,
|
||||||
|
}
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel.
|
||||||
|
func (b *Builder) Cancel() {
|
||||||
|
if b.runner != nil {
|
||||||
|
log.Println("Cancelling the step runner...")
|
||||||
|
b.runner.Cancel()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,210 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/common"
|
||||||
|
"github.com/hashicorp/packer/common/uuid"
|
||||||
|
"github.com/hashicorp/packer/helper/communicator"
|
||||||
|
"github.com/hashicorp/packer/helper/config"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reImageFamily = regexp.MustCompile(`^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$`)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
Communicator communicator.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
|
Token string `mapstructure:"token"`
|
||||||
|
ServiceAccountKeyFile string `mapstructure:"service_account_key_file"`
|
||||||
|
FolderID string `mapstructure:"folder_id"`
|
||||||
|
Zone string `mapstructure:"zone"`
|
||||||
|
|
||||||
|
SerialLogFile string `mapstructure:"serial_log_file"`
|
||||||
|
InstanceCores int `mapstructure:"instance_cores"`
|
||||||
|
InstanceMemory int `mapstructure:"instance_mem_gb"`
|
||||||
|
DiskSizeGb int `mapstructure:"disk_size_gb"`
|
||||||
|
DiskType string `mapstructure:"disk_type"`
|
||||||
|
SubnetID string `mapstructure:"subnet_id"`
|
||||||
|
ImageName string `mapstructure:"image_name"`
|
||||||
|
ImageFamily string `mapstructure:"image_family"`
|
||||||
|
ImageDescription string `mapstructure:"image_description"`
|
||||||
|
ImageLabels map[string]string `mapstructure:"image_labels"`
|
||||||
|
ImageProductIDs []string `mapstructure:"image_product_ids"`
|
||||||
|
InstanceName string `mapstructure:"instance_name"`
|
||||||
|
Labels map[string]string `mapstructure:"labels"`
|
||||||
|
DiskName string `mapstructure:"disk_name"`
|
||||||
|
MachineType string `mapstructure:"machine_type"`
|
||||||
|
Metadata map[string]string `mapstructure:"metadata"`
|
||||||
|
SourceImageID string `mapstructure:"source_image_id"`
|
||||||
|
SourceImageFamily string `mapstructure:"source_image_family"`
|
||||||
|
SourceImageFolderID string `mapstructure:"source_image_folder_id"`
|
||||||
|
UseInternalIP bool `mapstructure:"use_internal_ip"`
|
||||||
|
UseIPv4Nat bool `mapstructure:"use_ipv4_nat"`
|
||||||
|
UseIPv6 bool `mapstructure:"use_ipv6"`
|
||||||
|
|
||||||
|
RawStepTimeout string `mapstructure:"step_timeout"`
|
||||||
|
|
||||||
|
stepTimeout time.Duration
|
||||||
|
ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||||
|
c := &Config{}
|
||||||
|
c.ctx.Funcs = TemplateFuncs
|
||||||
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
|
Interpolate: true,
|
||||||
|
InterpolateContext: &c.ctx,
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packer.MultiError
|
||||||
|
|
||||||
|
if c.SerialLogFile != "" {
|
||||||
|
if _, err := os.Stat(c.SerialLogFile); os.IsExist(err) {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Serial log file %s already exist", c.SerialLogFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InstanceCores == 0 {
|
||||||
|
c.InstanceCores = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InstanceMemory == 0 {
|
||||||
|
c.InstanceMemory = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DiskSizeGb == 0 {
|
||||||
|
c.DiskSizeGb = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DiskType == "" {
|
||||||
|
c.DiskType = "network-hdd"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ImageDescription == "" {
|
||||||
|
c.ImageDescription = "Created by Packer"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ImageName == "" {
|
||||||
|
img, err := interpolate.Render("packer-{{timestamp}}", nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Unable to render default image name: %s ", err))
|
||||||
|
} else {
|
||||||
|
c.ImageName = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.ImageFamily) > 63 {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
errors.New("Invalid image family: Must not be longer than 63 characters"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ImageFamily != "" {
|
||||||
|
if !reImageFamily.MatchString(c.ImageFamily) {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
errors.New("Invalid image family: The first character must be a "+
|
||||||
|
"lowercase letter, and all following characters must be a dash, "+
|
||||||
|
"lowercase letter, or digit, except the last character, which cannot be a dash"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InstanceName == "" {
|
||||||
|
c.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DiskName == "" {
|
||||||
|
c.DiskName = c.InstanceName + "-disk"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MachineType == "" {
|
||||||
|
c.MachineType = "standard-v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.RawStepTimeout == "" {
|
||||||
|
c.RawStepTimeout = "5m"
|
||||||
|
}
|
||||||
|
|
||||||
|
if es := c.Communicator.Prepare(&c.ctx); len(es) > 0 {
|
||||||
|
errs = packer.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process required parameters.
|
||||||
|
|
||||||
|
if c.SourceImageID == "" && c.SourceImageFamily == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("a source_image_id or source_image_family must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.CalcTimeout()
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Endpoint == "" {
|
||||||
|
c.Endpoint = "api.cloud.yandex.net:443"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Token == "" && c.ServiceAccountKeyFile == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("a token or service account key file must be specified"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Token != "" && c.ServiceAccountKeyFile != "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("one of token or service account key file must be specified, not both"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Token != "" {
|
||||||
|
packer.LogSecretFilter.Set(c.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ServiceAccountKeyFile != "" {
|
||||||
|
if _, err := iamkey.ReadFromJSONFile(c.ServiceAccountKeyFile); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("fail to parse service account key file: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.FolderID == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("a folder_id must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Zone == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("a zone must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any errors.
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) CalcTimeout() error {
|
||||||
|
stepTimeout, err := time.ParseDuration(c.RawStepTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed parsing step_timeout: %s", err)
|
||||||
|
}
|
||||||
|
c.stepTimeout = stepTimeout
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,244 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TestServiceAccountKeyFile = "./test_data/fake-sa-key.json"
|
||||||
|
|
||||||
|
func TestConfigPrepare(t *testing.T) {
|
||||||
|
tf, err := ioutil.TempFile("", "packer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tf.Name())
|
||||||
|
tf.Close()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
Key string
|
||||||
|
Value interface{}
|
||||||
|
Err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"unknown_key",
|
||||||
|
"bad",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"service_account_key_file",
|
||||||
|
"/tmp/i/should/not/exist",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"service_account_key_file",
|
||||||
|
tf.Name(),
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"service_account_key_file",
|
||||||
|
TestServiceAccountKeyFile,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"folder_id",
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"folder_id",
|
||||||
|
"foo",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"source_image_id",
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_image_id",
|
||||||
|
"foo",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"source_image_family",
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source_image_family",
|
||||||
|
"foo",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"zone",
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zone",
|
||||||
|
"foo",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"ssh_timeout",
|
||||||
|
"SO BAD",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ssh_timeout",
|
||||||
|
"5s",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"step_timeout",
|
||||||
|
"SO BAD",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_timeout",
|
||||||
|
"5s",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image_family",
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image_family",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image_family",
|
||||||
|
"foo-bar",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image_family",
|
||||||
|
"foo bar",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
raw := testConfig(t)
|
||||||
|
|
||||||
|
if tc.Value == nil {
|
||||||
|
delete(raw, tc.Key)
|
||||||
|
} else {
|
||||||
|
raw[tc.Key] = tc.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.Key == "service_account_key_file" {
|
||||||
|
delete(raw, "token")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, warns, errs := NewConfig(raw)
|
||||||
|
|
||||||
|
if tc.Err {
|
||||||
|
testConfigErr(t, warns, errs, tc.Key)
|
||||||
|
} else {
|
||||||
|
testConfigOk(t, warns, errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigDefaults(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Read func(c *Config) interface{}
|
||||||
|
Value interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
func(c *Config) interface{} { return c.Communicator.Type },
|
||||||
|
"ssh",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
func(c *Config) interface{} { return c.Communicator.SSHPort },
|
||||||
|
22,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
raw := testConfig(t)
|
||||||
|
|
||||||
|
c, warns, errs := NewConfig(raw)
|
||||||
|
testConfigOk(t, warns, errs)
|
||||||
|
|
||||||
|
actual := tc.Read(c)
|
||||||
|
if actual != tc.Value {
|
||||||
|
t.Fatalf("bad: %#v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageName(t *testing.T) {
|
||||||
|
raw := testConfig(t)
|
||||||
|
|
||||||
|
c, _, _ := NewConfig(raw)
|
||||||
|
if !strings.HasPrefix(c.ImageName, "packer-") {
|
||||||
|
t.Fatalf("ImageName should have 'packer-' prefix, found %s", c.ImageName)
|
||||||
|
}
|
||||||
|
if strings.Contains(c.ImageName, "{{timestamp}}") {
|
||||||
|
t.Errorf("ImageName should be interpolated; found %s", c.ImageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZone(t *testing.T) {
|
||||||
|
raw := testConfig(t)
|
||||||
|
|
||||||
|
c, _, _ := NewConfig(raw)
|
||||||
|
if c.Zone != "ru-central1-a" {
|
||||||
|
t.Fatalf("Zone should be 'ru-central1-a' given, but is '%s'", c.Zone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper stuff below
|
||||||
|
|
||||||
|
func testConfig(t *testing.T) (config map[string]interface{}) {
|
||||||
|
|
||||||
|
config = map[string]interface{}{
|
||||||
|
"token": "test_token",
|
||||||
|
"folder_id": "hashicorp",
|
||||||
|
"source_image_id": "foo",
|
||||||
|
"ssh_username": "root",
|
||||||
|
"image_family": "bar",
|
||||||
|
"image_product_ids": []string{
|
||||||
|
"test-license",
|
||||||
|
},
|
||||||
|
"zone": "ru-central1-a",
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfigErr(t *testing.T, warns []string, err error, extra string) {
|
||||||
|
if len(warns) > 0 {
|
||||||
|
t.Fatalf("bad: %#v", warns)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("should error: %s", extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfigOk(t *testing.T, warns []string, err error) {
|
||||||
|
if len(warns) > 0 {
|
||||||
|
t.Fatalf("bad: %#v", warns)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/helper/useragent"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
|
||||||
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
|
"github.com/yandex-cloud/go-sdk/iamkey"
|
||||||
|
"github.com/yandex-cloud/go-sdk/pkg/requestid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Driver interface {
|
||||||
|
DeleteImage(id string) error
|
||||||
|
SDK() *ycsdk.SDK
|
||||||
|
GetImage(imageID string) (*Image, error)
|
||||||
|
GetImageFromFolder(folderID string, family string) (*Image, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type driverYC struct {
|
||||||
|
sdk *ycsdk.SDK
|
||||||
|
ui packer.Ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverYC) GetImage(imageID string) (*Image, error) {
|
||||||
|
image, err := d.sdk.Compute().Image().Get(context.Background(), &compute.GetImageRequest{
|
||||||
|
ImageId: imageID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Image{
|
||||||
|
ID: image.Id,
|
||||||
|
Labels: image.Labels,
|
||||||
|
Licenses: image.ProductIds,
|
||||||
|
Name: image.Name,
|
||||||
|
FolderID: image.FolderId,
|
||||||
|
MinDiskSizeGb: toGigabytes(image.MinDiskSize),
|
||||||
|
SizeGb: toGigabytes(image.StorageSize),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverYC) GetImageFromFolder(folderID string, family string) (*Image, error) {
|
||||||
|
image, err := d.sdk.Compute().Image().GetLatestByFamily(context.Background(), &compute.GetImageLatestByFamilyRequest{
|
||||||
|
FolderId: folderID,
|
||||||
|
Family: family,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Image{
|
||||||
|
ID: image.Id,
|
||||||
|
Labels: image.Labels,
|
||||||
|
Licenses: image.ProductIds,
|
||||||
|
Name: image.Name,
|
||||||
|
FolderID: image.FolderId,
|
||||||
|
MinDiskSizeGb: toGigabytes(image.MinDiskSize),
|
||||||
|
SizeGb: toGigabytes(image.StorageSize),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDriverYandexCloud(ui packer.Ui, config *Config) (Driver, error) {
|
||||||
|
log.Printf("[INFO] Initialize Yandex Cloud client...")
|
||||||
|
|
||||||
|
sdkConfig := ycsdk.Config{}
|
||||||
|
|
||||||
|
if config.Endpoint != "" {
|
||||||
|
sdkConfig.Endpoint = config.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case config.Token != "":
|
||||||
|
sdkConfig.Credentials = ycsdk.OAuthToken(config.Token)
|
||||||
|
|
||||||
|
case config.ServiceAccountKeyFile != "":
|
||||||
|
key, err := iamkey.ReadFromJSONFile(config.ServiceAccountKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := ycsdk.ServiceAccountKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sdkConfig.Credentials = credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgentMD := metadata.Pairs("user-agent", useragent.String())
|
||||||
|
|
||||||
|
sdk, err := ycsdk.Build(context.Background(), sdkConfig,
|
||||||
|
grpc.WithDefaultCallOptions(grpc.Header(&userAgentMD)),
|
||||||
|
grpc.WithUnaryInterceptor(requestid.Interceptor()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = sdk.ApiEndpoint().ApiEndpoint().List(context.Background(), &endpoint.ListApiEndpointsRequest{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &driverYC{
|
||||||
|
sdk: sdk,
|
||||||
|
ui: ui,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverYC) DeleteImage(ID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverYC) SDK() *ycsdk.SDK {
|
||||||
|
return d.sdk
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
ID string
|
||||||
|
FolderID string
|
||||||
|
Labels map[string]string
|
||||||
|
Licenses []string
|
||||||
|
MinDiskSizeGb int
|
||||||
|
Name string
|
||||||
|
SizeGb int
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commHost(state multistep.StateBag) (string, error) {
|
||||||
|
ipAddress := state.Get("instance_ip").(string)
|
||||||
|
return ipAddress, nil
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateImage struct{}
|
||||||
|
|
||||||
|
func (stepCreateImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
sdk := state.Get("sdk").(*ycsdk.SDK)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
diskID := state.Get("disk_id").(string)
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Creating image: %v", c.ImageName))
|
||||||
|
op, err := sdk.WrapOperation(sdk.Compute().Image().Create(context.Background(), &compute.CreateImageRequest{
|
||||||
|
FolderId: c.FolderID,
|
||||||
|
Name: c.ImageName,
|
||||||
|
Family: c.ImageFamily,
|
||||||
|
Description: c.ImageDescription,
|
||||||
|
Labels: c.ImageLabels,
|
||||||
|
ProductIds: c.ImageProductIDs,
|
||||||
|
Source: &compute.CreateImageRequest_DiskId{
|
||||||
|
DiskId: diskID,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error creating image: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the pending state over, verify that we're in the active state
|
||||||
|
ui.Say("Waiting for image to complete...")
|
||||||
|
if err := op.Wait(context.Background()); err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error waiting for image: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := op.Response()
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
image, ok := resp.(*compute.Image)
|
||||||
|
if !ok {
|
||||||
|
return stepHaltWithError(state, errors.New("Response doesn't contain Image"))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Image ID: %s", image.Id)
|
||||||
|
log.Printf("Image Name: %s", image.Name)
|
||||||
|
log.Printf("Image Family: %s", image.Family)
|
||||||
|
log.Printf("Image Description: %s", image.Description)
|
||||||
|
log.Printf("Image Storage size: %d", image.StorageSize)
|
||||||
|
state.Put("image_id", image.Id)
|
||||||
|
state.Put("image_name", c.ImageName)
|
||||||
|
state.Put("image_family", c.ImageFamily)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stepCreateImage) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
|
"github.com/hashicorp/packer/common/uuid"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/vpc/v1"
|
||||||
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateInstance struct {
|
||||||
|
Debug bool
|
||||||
|
SerialLogFile string
|
||||||
|
cleanupInstanceID string
|
||||||
|
cleanupNetworkID string
|
||||||
|
cleanupSubnetID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNetwork(c *Config, d Driver) (*vpc.Network, error) {
|
||||||
|
req := &vpc.CreateNetworkRequest{
|
||||||
|
FolderId: c.FolderID,
|
||||||
|
Name: fmt.Sprintf("packer-network-%s", uuid.TimeOrderedUUID()),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sdk := d.SDK()
|
||||||
|
|
||||||
|
op, err := sdk.WrapOperation(sdk.VPC().Network().Create(ctx, req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = op.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := op.Response()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
network, ok := resp.(*vpc.Network)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("network create operation response doesn't contain Network")
|
||||||
|
}
|
||||||
|
return network, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSubnet(c *Config, d Driver, networkID string) (*vpc.Subnet, error) {
|
||||||
|
req := &vpc.CreateSubnetRequest{
|
||||||
|
FolderId: c.FolderID,
|
||||||
|
NetworkId: networkID,
|
||||||
|
Name: fmt.Sprintf("packer-subnet-%s", uuid.TimeOrderedUUID()),
|
||||||
|
ZoneId: c.Zone,
|
||||||
|
V4CidrBlocks: []string{"192.168.111.0/24"},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sdk := d.SDK()
|
||||||
|
|
||||||
|
op, err := sdk.WrapOperation(sdk.VPC().Subnet().Create(ctx, req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = op.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := op.Response()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
network, ok := resp.(*vpc.Subnet)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("subnet create operation response doesn't contain Network")
|
||||||
|
}
|
||||||
|
return network, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImage(c *Config, d Driver) (*Image, error) {
|
||||||
|
if c.SourceImageID != "" {
|
||||||
|
return d.GetImage(c.SourceImageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
familyName := c.SourceImageFamily
|
||||||
|
if c.SourceImageFolderID != "" {
|
||||||
|
return d.GetImageFromFolder(c.SourceImageFolderID, familyName)
|
||||||
|
}
|
||||||
|
return d.GetImageFromFolder("standard-images", familyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
sdk := state.Get("sdk").(*ycsdk.SDK)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
d := state.Get("driver").(Driver)
|
||||||
|
|
||||||
|
// create or reuse Subnet
|
||||||
|
instanceSubnetID := ""
|
||||||
|
if c.SubnetID == "" {
|
||||||
|
// create Network
|
||||||
|
ui.Say("Creating network...")
|
||||||
|
network, err := createNetwork(c, d)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error creating network: %s", err))
|
||||||
|
}
|
||||||
|
state.Put("network_id", network.Id)
|
||||||
|
s.cleanupNetworkID = network.Id
|
||||||
|
|
||||||
|
ui.Say("Creating subnet in zone...")
|
||||||
|
subnet, err := createSubnet(c, d, network.Id)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error creating subnet: %s", err))
|
||||||
|
}
|
||||||
|
state.Put("subnet_id", subnet.Id)
|
||||||
|
instanceSubnetID = subnet.Id
|
||||||
|
// save for cleanup
|
||||||
|
s.cleanupSubnetID = subnet.Id
|
||||||
|
} else {
|
||||||
|
ui.Say("Use provided subnet id " + c.SubnetID)
|
||||||
|
instanceSubnetID = c.SubnetID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an instance based on the configuration
|
||||||
|
ui.Say("Creating instance...")
|
||||||
|
sourceImage, err := getImage(c, d)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error getting source image for instance creation: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if sourceImage.MinDiskSizeGb > c.DiskSizeGb {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Instance DiskSizeGb (%d) should be equal or greater "+
|
||||||
|
"than SourceImage disk requirement (%d)", c.DiskSizeGb, sourceImage.MinDiskSizeGb))
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceMetadata, err := c.createInstanceMetadata(string(c.Communicator.SSHPublicKey))
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("instance metadata prepare error: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO make part metadata prepare process
|
||||||
|
if c.UseIPv6 {
|
||||||
|
// this ugly hack will replace user provided 'user-data'
|
||||||
|
userData := `#cloud-config
|
||||||
|
runcmd:
|
||||||
|
- [ sh, -c, '/sbin/dhclient -6 -D LL -nw -pf /run/dhclient_ipv6.eth0.pid -lf /var/lib/dhcp/dhclient_ipv6.eth0.leases eth0' ]
|
||||||
|
`
|
||||||
|
instanceMetadata["user-data"] = userData
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &compute.CreateInstanceRequest{
|
||||||
|
FolderId: c.FolderID,
|
||||||
|
Name: c.InstanceName,
|
||||||
|
Labels: c.Labels,
|
||||||
|
ZoneId: c.Zone,
|
||||||
|
PlatformId: "standard-v1",
|
||||||
|
ResourcesSpec: &compute.ResourcesSpec{
|
||||||
|
Memory: toBytes(c.InstanceMemory),
|
||||||
|
Cores: int64(c.InstanceCores),
|
||||||
|
},
|
||||||
|
Metadata: instanceMetadata,
|
||||||
|
BootDiskSpec: &compute.AttachedDiskSpec{
|
||||||
|
AutoDelete: true,
|
||||||
|
Disk: &compute.AttachedDiskSpec_DiskSpec_{
|
||||||
|
DiskSpec: &compute.AttachedDiskSpec_DiskSpec{
|
||||||
|
Name: c.DiskName,
|
||||||
|
TypeId: c.DiskType,
|
||||||
|
Size: int64((datasize.ByteSize(c.DiskSizeGb) * datasize.GB).Bytes()),
|
||||||
|
Source: &compute.AttachedDiskSpec_DiskSpec_ImageId{
|
||||||
|
ImageId: sourceImage.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NetworkInterfaceSpecs: []*compute.NetworkInterfaceSpec{
|
||||||
|
{
|
||||||
|
SubnetId: instanceSubnetID,
|
||||||
|
PrimaryV4AddressSpec: &compute.PrimaryAddressSpec{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseIPv6 {
|
||||||
|
req.NetworkInterfaceSpecs[0].PrimaryV6AddressSpec = &compute.PrimaryAddressSpec{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseIPv4Nat {
|
||||||
|
req.NetworkInterfaceSpecs[0].PrimaryV4AddressSpec = &compute.PrimaryAddressSpec{
|
||||||
|
OneToOneNatSpec: &compute.OneToOneNatSpec{
|
||||||
|
IpVersion: compute.IpVersion_IPV4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
op, err := sdk.WrapOperation(sdk.Compute().Instance().Create(ctx, req))
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error create instance: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = op.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error create instance: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := op.Response()
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
instance, ok := resp.(*compute.Instance)
|
||||||
|
if !ok {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("response doesn't contain Instance"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use this in cleanup
|
||||||
|
s.cleanupInstanceID = instance.Id
|
||||||
|
|
||||||
|
if s.Debug {
|
||||||
|
ui.Message(fmt.Sprintf("Instance ID %s started. Current status %s", instance.Id, instance.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the instance id for later
|
||||||
|
state.Put("instance_id", instance.Id)
|
||||||
|
state.Put("disk_id", instance.BootDisk.DiskId)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
|
||||||
|
// If the cleanupInstanceID isn't there, we probably never created it
|
||||||
|
if s.cleanupInstanceID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk := state.Get("sdk").(*ycsdk.SDK)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
if s.SerialLogFile != "" {
|
||||||
|
ui.Say("Current state 'cancelled' or 'halted'...")
|
||||||
|
err := s.writeSerialLogFile(state)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the instance we just created
|
||||||
|
ui.Say("Destroying instance...")
|
||||||
|
|
||||||
|
_, err := sdk.Compute().Instance().Delete(context.Background(), &compute.DeleteInstanceRequest{
|
||||||
|
InstanceId: s.cleanupInstanceID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf(
|
||||||
|
"Error destroying instance (id: %s): %s.\nPlease destroy it manually", s.cleanupInstanceID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cleanupSubnetID != "" {
|
||||||
|
// some sleep before delete network components
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
|
// Destroy the subnet we just created
|
||||||
|
ui.Say("Destroying subnet...")
|
||||||
|
_, err = sdk.VPC().Subnet().Delete(context.Background(), &vpc.DeleteSubnetRequest{
|
||||||
|
SubnetId: s.cleanupSubnetID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf(
|
||||||
|
"Error destroying subnet (id: %s). Please destroy it manually: %s", s.cleanupSubnetID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// some sleep before delete network
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
// Destroy the network we just created
|
||||||
|
ui.Say("Destroying network...")
|
||||||
|
_, err = sdk.VPC().Network().Delete(context.Background(), &vpc.DeleteNetworkRequest{
|
||||||
|
NetworkId: s.cleanupNetworkID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf(
|
||||||
|
"Error destroying network (id: %s). Please destroy it manually: %s", s.cleanupNetworkID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func (s *stepCreateInstance) writeSerialLogFile(state multistep.StateBag) error {
|
||||||
|
sdk := state.Get("sdk").(*ycsdk.SDK)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
ui.Say("Try get serial port output to file " + s.SerialLogFile)
|
||||||
|
serialOutput, err := sdk.Compute().Instance().GetSerialPortOutput(context.Background(), &compute.GetInstanceSerialPortOutputRequest{
|
||||||
|
InstanceId: s.cleanupInstanceID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get serial port output for instance (id: %s): %s", s.cleanupInstanceID, err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(s.SerialLogFile, []byte(serialOutput.Contents), 0600); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write serial port output to file: %s", err)
|
||||||
|
}
|
||||||
|
ui.Message("Serial port output has been successfully written")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) createInstanceMetadata(sshPublicKey string) (map[string]string, error) {
|
||||||
|
instanceMetadata := make(map[string]string)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Copy metadata from config.
|
||||||
|
for k, v := range c.Metadata {
|
||||||
|
instanceMetadata[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if sshPublicKey != "" {
|
||||||
|
sshMetaKey := "ssh-keys"
|
||||||
|
sshKeys := fmt.Sprintf("%s:%s", c.Communicator.SSHUsername, sshPublicKey)
|
||||||
|
if confSSHKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
||||||
|
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSSHKeys)
|
||||||
|
}
|
||||||
|
instanceMetadata[sshMetaKey] = sshKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
return instanceMetadata, err
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/common/uuid"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateSSHKey struct {
|
||||||
|
Debug bool
|
||||||
|
DebugKeyPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
|
||||||
|
if c.Communicator.SSHPrivateKeyFile != "" {
|
||||||
|
ui.Say("Using existing SSH private key")
|
||||||
|
privateKeyBytes, err := c.Communicator.ReadSSHPrivateKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := ssh.ParsePrivateKey(privateKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error parsing 'ssh_private_key_file': %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Communicator.SSHPublicKey = ssh.MarshalAuthorizedKey(key.PublicKey())
|
||||||
|
c.Communicator.SSHPrivateKey = privateKeyBytes
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Creating temporary ssh key for instance...")
|
||||||
|
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error generating temporary SSH key: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASN.1 DER encoded form
|
||||||
|
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privBlk := pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Headers: nil,
|
||||||
|
Bytes: privDer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the public key into SSH compatible format
|
||||||
|
// TODO properly handle the public key error
|
||||||
|
pub, _ := ssh.NewPublicKey(&priv.PublicKey)
|
||||||
|
pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub))
|
||||||
|
|
||||||
|
// The name of the public key on DO
|
||||||
|
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||||
|
|
||||||
|
hashMd5 := ssh.FingerprintLegacyMD5(pub)
|
||||||
|
hashSha256 := ssh.FingerprintSHA256(pub)
|
||||||
|
|
||||||
|
log.Printf("[INFO] temporary ssh key name: %s", name)
|
||||||
|
log.Printf("[INFO] md5 hash of ssh pub key: %s", hashMd5)
|
||||||
|
log.Printf("[INFO] sha256 hash of ssh pub key: %s", hashSha256)
|
||||||
|
|
||||||
|
// Remember some state for the future
|
||||||
|
//state.Put("ssh_key_id", key.ID)
|
||||||
|
state.Put("ssh_key_public", pubSSHFormat)
|
||||||
|
state.Put("ssh_key_name", name)
|
||||||
|
|
||||||
|
// Set the private key in the config for later
|
||||||
|
c.Communicator.SSHPrivateKey = pem.EncodeToMemory(&privBlk)
|
||||||
|
c.Communicator.SSHPublicKey = ssh.MarshalAuthorizedKey(pub)
|
||||||
|
|
||||||
|
// If we're in debug mode, output the private key to the working directory.
|
||||||
|
if s.Debug {
|
||||||
|
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||||
|
err := ioutil.WriteFile(s.DebugKeyPath, c.Communicator.SSHPrivateKey, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error saving debug key: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepInstanceInfo struct{}
|
||||||
|
|
||||||
|
func (s *stepInstanceInfo) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
sdk := state.Get("sdk").(*ycsdk.SDK)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
instanceID := state.Get("instance_id").(string)
|
||||||
|
|
||||||
|
ui.Say("Waiting for instance to become active...")
|
||||||
|
|
||||||
|
// Set the IP on the state for later
|
||||||
|
instance, err := sdk.Compute().Instance().Get(context.Background(), &compute.GetInstanceRequest{
|
||||||
|
InstanceId: instanceID,
|
||||||
|
View: compute.InstanceView_FULL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error retrieving instance data: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceIP, err := getInstanceIPAddress(c, instance)
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Failed to find instance ip address: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("instance_ip", instanceIP)
|
||||||
|
ui.Message(fmt.Sprintf("Detected instance IP: %s", instanceIP))
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstanceIPAddress(c *Config, instance *compute.Instance) (address string, err error) {
|
||||||
|
// Instance could have several network interfaces with different configuration each
|
||||||
|
// Get all possible addresses for instance
|
||||||
|
addrIPV4Internal, addrIPV4External, addrIPV6Addr, err := instanceAddresses(instance)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseIPv6 {
|
||||||
|
if addrIPV6Addr != "" {
|
||||||
|
return "[" + addrIPV6Addr + "]", nil
|
||||||
|
}
|
||||||
|
return "", errors.New("instance has no one IPv6 address")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseInternalIP {
|
||||||
|
if addrIPV4Internal != "" {
|
||||||
|
return addrIPV4Internal, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("instance has no one IPv4 internal address")
|
||||||
|
}
|
||||||
|
if addrIPV4External != "" {
|
||||||
|
return addrIPV4External, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("instance has no one IPv4 external address")
|
||||||
|
}
|
||||||
|
|
||||||
|
func instanceAddresses(instance *compute.Instance) (ipV4Int, ipV4Ext, ipV6 string, err error) {
|
||||||
|
if len(instance.NetworkInterfaces) == 0 {
|
||||||
|
return "", "", "", errors.New("No one network interface found for an instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipV4IntFound, ipV4ExtFound, ipV6Found bool
|
||||||
|
for _, iface := range instance.NetworkInterfaces {
|
||||||
|
if !ipV6Found && iface.PrimaryV6Address != nil {
|
||||||
|
ipV6 = iface.PrimaryV6Address.Address
|
||||||
|
ipV6Found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ipV4IntFound && iface.PrimaryV4Address != nil {
|
||||||
|
ipV4Int = iface.PrimaryV4Address.Address
|
||||||
|
ipV4IntFound = true
|
||||||
|
|
||||||
|
if !ipV4ExtFound && iface.PrimaryV4Address.OneToOneNat != nil {
|
||||||
|
ipV4Ext = iface.PrimaryV4Address.OneToOneNat.Address
|
||||||
|
ipV4ExtFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipV6Found && ipV4IntFound && ipV4ExtFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ipV4IntFound {
|
||||||
|
// internal ipV4 address always should present
|
||||||
|
return "", "", "", errors.New("No IPv4 internal address found. Bug?")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepInstanceInfo) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepShutdown struct {
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepShutdown) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
sdk := state.Get("sdk").(*ycsdk.SDK)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
instanceID := state.Get("instance_id").(string)
|
||||||
|
|
||||||
|
// Gracefully power off the instance. We have to retry this a number
|
||||||
|
// of times because sometimes it says it completed when it actually
|
||||||
|
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
|
||||||
|
// a pretty arbitrary amount of time.
|
||||||
|
ui.Say("Gracefully shutting down instance...")
|
||||||
|
op, err := sdk.WrapOperation(sdk.Compute().Instance().Stop(context.Background(), &compute.StopInstanceRequest{
|
||||||
|
InstanceId: instanceID,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error shutting down instance: %s", err))
|
||||||
|
}
|
||||||
|
err = op.Wait(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return stepHaltWithError(state, fmt.Errorf("Error shutting down instance: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Debug {
|
||||||
|
ui.Message("Instance status before image create:")
|
||||||
|
displayInstanceStatus(sdk, instanceID, ui)
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepShutdown) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
import "text/template"
|
||||||
|
|
||||||
|
func isalphanumeric(b byte) bool {
|
||||||
|
if '0' <= b && b <= '9' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if 'a' <= b && b <= 'z' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up image name by replacing invalid characters with "-"
|
||||||
|
// and converting upper cases to lower cases
|
||||||
|
func templateCleanImageName(s string) string {
|
||||||
|
if reImageFamily.MatchString(s) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
b := []byte(strings.ToLower(s))
|
||||||
|
newb := make([]byte, len(b))
|
||||||
|
for i := range newb {
|
||||||
|
if isalphanumeric(b[i]) {
|
||||||
|
newb[i] = b[i]
|
||||||
|
} else {
|
||||||
|
newb[i] = '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(newb)
|
||||||
|
}
|
||||||
|
|
||||||
|
var TemplateFuncs = template.FuncMap{
|
||||||
|
"clean_image_name": templateCleanImageName,
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"id": "ajeboa0du6edu6m43c3t",
|
||||||
|
"service_account_id": "ajeq7dsmihqple6761c5",
|
||||||
|
"created_at": "2018-11-19T13:38:09Z",
|
||||||
|
"description": "description",
|
||||||
|
"key_algorithm": "RSA_4096",
|
||||||
|
"public_key": "-----BEGIN PUBLIC KEY-----\nMIICCgKCAgEAo/s1lN5vFpFNJvS/l+yRilQHAPDeC3JqBwpLstbqJXW4kAUaKKoe\nxkIuJuPUKOUcd/JE3LXOEt/LOFb9mkCRdpjaIW7Jd5Fw0kTHIZ5rDoq7DZx0LV9b\nGJNskdccd6M6stb1GEqVuGpVcyXMCH8tMSG3c85DkcAg0cxXgyrirAzHMPiWSTpj\nJjICkxXRVj01Xq7dIDqL2LSMrZ2kLda5m+CnfscUbwnGRPPoEg20jLiEgBM2o43e\nhpWko1NStRR5fMQcQSUBbdtvbfPracjZz2/fq4fZfqlnObgq3WpYpdGynniLH3i5\nbxPM3ufYL3HY2w5aIOY6KIwMKLf3WYlug90ieviMYAvCukrCASwyqBQlt3MKCHlN\nIcebZXJDQ1VSBuEs+4qXYlhG1p+5C07zahzigNNTm6rEo47FFfClF04mv2uJN42F\nfWlEPR+V9JHBcfcBCdvyhiGzftl/vDo2NdO751ETIhyNKzxM/Ve2PR9h/qcuEatC\nLlXUA+40epNNHbSxAauxcngyrtkn7FZAEhdjyTtx46sELyb90Z56WgnbNUUGnsS/\nHBnBy5z8RyCmI5MjTC2NtplVqtAWkG+x59mU3GoCeuI8EaNtu2YPXhl1ovRkS4NB\n1G0F4c5FiJ27/E2MbNKlV5iw9ICcDforATYTeqiXbkkEKqIIiZYZWOsCAwEAAQ==\n-----END PUBLIC KEY-----\n",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\n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\n-----END PRIVATE KEY-----\n"
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package yandex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
|
"github.com/hashicorp/packer/helper/multistep"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stepHaltWithError(state multistep.StateBag, err error) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayInstanceStatus(sdk *ycsdk.SDK, instanceID string, ui packer.Ui) {
|
||||||
|
instance, err := sdk.Compute().Instance().Get(context.Background(), &compute.GetInstanceRequest{
|
||||||
|
InstanceId: instanceID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Fail to get instance data: %s", err))
|
||||||
|
}
|
||||||
|
ui.Message(fmt.Sprintf("Current instance status %s", instance.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGigabytes(bytesCount int64) int {
|
||||||
|
return int((datasize.ByteSize(bytesCount) * datasize.B).GBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBytes(gigabytesCount int) int64 {
|
||||||
|
return int64((datasize.ByteSize(gigabytesCount) * datasize.GB).Bytes())
|
||||||
|
}
|
Loading…
Reference in New Issue