yandex/update-dump-method
This commit is contained in:
parent
f5cbb56c14
commit
379a33de42
|
@ -16,7 +16,7 @@ const (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cloudInitIPv6Config = `#!/usr/bin/env bash
|
cloudInitIPv6Config = `#!/usr/bin/env bash
|
||||||
dhclient -6 -D LL -nw -pf /run/dhclient_ipv6.eth0.pid -lf /var/lib/dhcp/dhclient_ipv6.eth0.leases eth0
|
dhclient -6 eth0
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -232,3 +232,32 @@ func (c *InstanceConfig) Prepare(errs *packersdk.MultiError) *packersdk.MultiErr
|
||||||
}
|
}
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SourceImageConfig struct {
|
||||||
|
// The source image family to create the new image
|
||||||
|
// from. You can also specify source_image_id instead. Just one of a source_image_id or
|
||||||
|
// source_image_family must be specified. Example: `ubuntu-1804-lts`.
|
||||||
|
SourceImageFamily string `mapstructure:"source_image_family" required:"true"`
|
||||||
|
// The ID of the folder containing the source image.
|
||||||
|
SourceImageFolderID string `mapstructure:"source_image_folder_id" required:"false"`
|
||||||
|
// The source image ID to use to create the new image from.
|
||||||
|
SourceImageID string `mapstructure:"source_image_id" required:"false"`
|
||||||
|
// The source image name to use to create the new image
|
||||||
|
// from. Name will be looked up in `source_image_folder_id`.
|
||||||
|
SourceImageName string `mapstructure:"source_image_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SourceImageConfig) Prepare(errs *packersdk.MultiError) *packersdk.MultiError {
|
||||||
|
// Process required parameters.
|
||||||
|
if c.SourceImageID == "" {
|
||||||
|
if c.SourceImageFamily == "" && c.SourceImageName == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("a source_image_name or source_image_family must be specified"))
|
||||||
|
}
|
||||||
|
if c.SourceImageFamily != "" && c.SourceImageName != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("one of source_image_name or source_image_family must be specified, not both"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
package yandex
|
package yandex
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/packer-plugin-sdk/common"
|
"github.com/hashicorp/packer-plugin-sdk/common"
|
||||||
|
@ -22,20 +21,10 @@ type Config struct {
|
||||||
CommonConfig `mapstructure:",squash"`
|
CommonConfig `mapstructure:",squash"`
|
||||||
ImageConfig `mapstructure:",squash"`
|
ImageConfig `mapstructure:",squash"`
|
||||||
|
|
||||||
|
SourceImageConfig `mapstructure:",squash"`
|
||||||
// Service account identifier to assign to instance.
|
// Service account identifier to assign to instance.
|
||||||
ServiceAccountID string `mapstructure:"service_account_id" required:"false"`
|
ServiceAccountID string `mapstructure:"service_account_id" required:"false"`
|
||||||
|
|
||||||
// The source image family to create the new image
|
|
||||||
// from. You can also specify source_image_id instead. Just one of a source_image_id or
|
|
||||||
// source_image_family must be specified. Example: `ubuntu-1804-lts`.
|
|
||||||
SourceImageFamily string `mapstructure:"source_image_family" required:"true"`
|
|
||||||
// The ID of the folder containing the source image.
|
|
||||||
SourceImageFolderID string `mapstructure:"source_image_folder_id" required:"false"`
|
|
||||||
// The source image ID to use to create the new image from.
|
|
||||||
SourceImageID string `mapstructure:"source_image_id" required:"false"`
|
|
||||||
// The source image name to use to create the new image
|
|
||||||
// from. Name will be looked up in `source_image_folder_id`.
|
|
||||||
SourceImageName string `mapstructure:"source_image_name"`
|
|
||||||
// The ID of the folder to save built image in.
|
// The ID of the folder to save built image in.
|
||||||
// This defaults to value of 'folder_id'.
|
// This defaults to value of 'folder_id'.
|
||||||
TargetImageFolderID string `mapstructure:"target_image_folder_id" required:"false"`
|
TargetImageFolderID string `mapstructure:"target_image_folder_id" required:"false"`
|
||||||
|
@ -60,6 +49,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
|
|
||||||
errs = c.CommonConfig.Prepare(errs)
|
errs = c.CommonConfig.Prepare(errs)
|
||||||
errs = c.ImageConfig.Prepare(errs)
|
errs = c.ImageConfig.Prepare(errs)
|
||||||
|
errs = c.SourceImageConfig.Prepare(errs)
|
||||||
|
|
||||||
if c.ImageMinDiskSizeGb == 0 {
|
if c.ImageMinDiskSizeGb == 0 {
|
||||||
c.ImageMinDiskSizeGb = c.DiskSizeGb
|
c.ImageMinDiskSizeGb = c.DiskSizeGb
|
||||||
|
@ -79,18 +69,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
errs = packersdk.MultiErrorAppend(errs, es...)
|
errs = packersdk.MultiErrorAppend(errs, es...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process required parameters.
|
|
||||||
if c.SourceImageID == "" {
|
|
||||||
if c.SourceImageFamily == "" && c.SourceImageName == "" {
|
|
||||||
errs = packersdk.MultiErrorAppend(
|
|
||||||
errs, errors.New("a source_image_name or source_image_family must be specified"))
|
|
||||||
}
|
|
||||||
if c.SourceImageFamily != "" && c.SourceImageName != "" {
|
|
||||||
errs = packersdk.MultiErrorAppend(
|
|
||||||
errs, errors.New("one of source_image_name or source_image_family must be specified, not both"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.TargetImageFolderID == "" {
|
if c.TargetImageFolderID == "" {
|
||||||
c.TargetImageFolderID = c.FolderID
|
c.TargetImageFolderID = c.FolderID
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,11 +98,11 @@ type FlatConfig struct {
|
||||||
ImageLabels map[string]string `mapstructure:"image_labels" required:"false" cty:"image_labels" hcl:"image_labels"`
|
ImageLabels map[string]string `mapstructure:"image_labels" required:"false" cty:"image_labels" hcl:"image_labels"`
|
||||||
ImageMinDiskSizeGb *int `mapstructure:"image_min_disk_size_gb" required:"false" cty:"image_min_disk_size_gb" hcl:"image_min_disk_size_gb"`
|
ImageMinDiskSizeGb *int `mapstructure:"image_min_disk_size_gb" required:"false" cty:"image_min_disk_size_gb" hcl:"image_min_disk_size_gb"`
|
||||||
ImageProductIDs []string `mapstructure:"image_product_ids" required:"false" cty:"image_product_ids" hcl:"image_product_ids"`
|
ImageProductIDs []string `mapstructure:"image_product_ids" required:"false" cty:"image_product_ids" hcl:"image_product_ids"`
|
||||||
ServiceAccountID *string `mapstructure:"service_account_id" required:"false" cty:"service_account_id" hcl:"service_account_id"`
|
|
||||||
SourceImageFamily *string `mapstructure:"source_image_family" required:"true" cty:"source_image_family" hcl:"source_image_family"`
|
SourceImageFamily *string `mapstructure:"source_image_family" required:"true" cty:"source_image_family" hcl:"source_image_family"`
|
||||||
SourceImageFolderID *string `mapstructure:"source_image_folder_id" required:"false" cty:"source_image_folder_id" hcl:"source_image_folder_id"`
|
SourceImageFolderID *string `mapstructure:"source_image_folder_id" required:"false" cty:"source_image_folder_id" hcl:"source_image_folder_id"`
|
||||||
SourceImageID *string `mapstructure:"source_image_id" required:"false" cty:"source_image_id" hcl:"source_image_id"`
|
SourceImageID *string `mapstructure:"source_image_id" required:"false" cty:"source_image_id" hcl:"source_image_id"`
|
||||||
SourceImageName *string `mapstructure:"source_image_name" cty:"source_image_name" hcl:"source_image_name"`
|
SourceImageName *string `mapstructure:"source_image_name" cty:"source_image_name" hcl:"source_image_name"`
|
||||||
|
ServiceAccountID *string `mapstructure:"service_account_id" required:"false" cty:"service_account_id" hcl:"service_account_id"`
|
||||||
TargetImageFolderID *string `mapstructure:"target_image_folder_id" required:"false" cty:"target_image_folder_id" hcl:"target_image_folder_id"`
|
TargetImageFolderID *string `mapstructure:"target_image_folder_id" required:"false" cty:"target_image_folder_id" hcl:"target_image_folder_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,11 +206,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
"image_labels": &hcldec.AttrSpec{Name: "image_labels", Type: cty.Map(cty.String), Required: false},
|
"image_labels": &hcldec.AttrSpec{Name: "image_labels", Type: cty.Map(cty.String), Required: false},
|
||||||
"image_min_disk_size_gb": &hcldec.AttrSpec{Name: "image_min_disk_size_gb", Type: cty.Number, Required: false},
|
"image_min_disk_size_gb": &hcldec.AttrSpec{Name: "image_min_disk_size_gb", Type: cty.Number, Required: false},
|
||||||
"image_product_ids": &hcldec.AttrSpec{Name: "image_product_ids", Type: cty.List(cty.String), Required: false},
|
"image_product_ids": &hcldec.AttrSpec{Name: "image_product_ids", Type: cty.List(cty.String), Required: false},
|
||||||
"service_account_id": &hcldec.AttrSpec{Name: "service_account_id", Type: cty.String, Required: false},
|
|
||||||
"source_image_family": &hcldec.AttrSpec{Name: "source_image_family", Type: cty.String, Required: false},
|
"source_image_family": &hcldec.AttrSpec{Name: "source_image_family", Type: cty.String, Required: false},
|
||||||
"source_image_folder_id": &hcldec.AttrSpec{Name: "source_image_folder_id", Type: cty.String, Required: false},
|
"source_image_folder_id": &hcldec.AttrSpec{Name: "source_image_folder_id", Type: cty.String, Required: false},
|
||||||
"source_image_id": &hcldec.AttrSpec{Name: "source_image_id", Type: cty.String, Required: false},
|
"source_image_id": &hcldec.AttrSpec{Name: "source_image_id", Type: cty.String, Required: false},
|
||||||
"source_image_name": &hcldec.AttrSpec{Name: "source_image_name", Type: cty.String, Required: false},
|
"source_image_name": &hcldec.AttrSpec{Name: "source_image_name", Type: cty.String, Required: false},
|
||||||
|
"service_account_id": &hcldec.AttrSpec{Name: "service_account_id", Type: cty.String, Required: false},
|
||||||
"target_image_folder_id": &hcldec.AttrSpec{Name: "target_image_folder_id", Type: cty.String, Required: false},
|
"target_image_folder_id": &hcldec.AttrSpec{Name: "target_image_folder_id", Type: cty.String, Required: false},
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
// CODE GENERATED. DO NOT EDIT
|
|
||||||
package yandexexport
|
|
||||||
|
|
||||||
var (
|
|
||||||
CloudInitScript = `#!/usr/bin/env bash
|
|
||||||
|
|
||||||
GetMetadata() {
|
|
||||||
curl -f -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/$1 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ "$(GetMetadata debug)" == "1" || "$(GetMetadata debug)" == "true" ]] && set -x
|
|
||||||
|
|
||||||
InstallPackages() {
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y qemu-utils awscli
|
|
||||||
}
|
|
||||||
|
|
||||||
WaitFile() {
|
|
||||||
local RETRIES=60
|
|
||||||
while [[ ${RETRIES} -gt 0 ]]; do
|
|
||||||
echo "Wait ${1}"
|
|
||||||
if [ -e "${1}" ]; then
|
|
||||||
echo "[${1}] has been found"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
RETRIES=$((RETRIES-1))
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo "[${1}] not found"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
PATHS=$(GetMetadata paths)
|
|
||||||
S3_ENDPOINT="https://storage.yandexcloud.net"
|
|
||||||
DISK_EXPORT_PATH="/dev/disk/by-id/virtio-doexport"
|
|
||||||
export AWS_SHARED_CREDENTIALS_FILE="/tmp/aws-credentials"
|
|
||||||
export AWS_REGION=ru-central1
|
|
||||||
|
|
||||||
Exit() {
|
|
||||||
for i in ${PATHS}; do
|
|
||||||
LOGDEST="${i}.exporter.log"
|
|
||||||
echo "Uploading exporter log to ${LOGDEST}..."
|
|
||||||
aws s3 --endpoint-url="${S3_ENDPOINT}" cp /var/log/syslog "${LOGDEST}"
|
|
||||||
done
|
|
||||||
|
|
||||||
exit $1
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallPackages
|
|
||||||
|
|
||||||
echo "####### Export configuration #######"
|
|
||||||
echo "Export paths - ${PATHS}"
|
|
||||||
echo "####################################"
|
|
||||||
|
|
||||||
if ! WaitFile "${AWS_SHARED_CREDENTIALS_FILE}"; then
|
|
||||||
echo "Failed wait credentials"
|
|
||||||
Exit 1
|
|
||||||
fi
|
|
||||||
udevadm trigger || true
|
|
||||||
|
|
||||||
if ! WaitFile "${DISK_EXPORT_PATH}"; then
|
|
||||||
echo "Failed wait attach disk"
|
|
||||||
Exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Dumping disk..."
|
|
||||||
if ! qemu-img convert -O qcow2 -o cluster_size=2M "${DISK_EXPORT_PATH}" disk.qcow2; then
|
|
||||||
echo "Failed to dump disk to qcow2 image."
|
|
||||||
Exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in ${PATHS}; do
|
|
||||||
echo "Uploading qcow2 disk image to ${i}..."
|
|
||||||
if ! aws s3 --endpoint-url="${S3_ENDPOINT}" cp disk.qcow2 "${i}"; then
|
|
||||||
echo "Failed to upload image to ${i}."
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
Exit ${FAIL}
|
|
||||||
`
|
|
||||||
)
|
|
|
@ -1,26 +1,31 @@
|
||||||
//go:generate struct-markdown
|
//go:generate struct-markdown
|
||||||
//go:generate mapstructure-to-hcl2 -type Config
|
//go:generate mapstructure-to-hcl2 -type Config
|
||||||
//go:generate go run ./scripts/script-to-var.go ./scripts/export.sh CloudInitScript cloud-init-script.go
|
|
||||||
|
|
||||||
package yandexexport
|
package yandexexport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/hashicorp/hcl/v2/hcldec"
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/common"
|
"github.com/hashicorp/packer-plugin-sdk/common"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/packerbuilderdata"
|
"github.com/hashicorp/packer-plugin-sdk/packerbuilderdata"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||||
|
"github.com/hashicorp/packer/builder/file"
|
||||||
"github.com/hashicorp/packer/builder/yandex"
|
"github.com/hashicorp/packer/builder/yandex"
|
||||||
"github.com/hashicorp/packer/post-processor/artifice"
|
"github.com/hashicorp/packer/post-processor/artifice"
|
||||||
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/compute/v1"
|
||||||
"github.com/yandex-cloud/go-genproto/yandex/cloud/iam/v1"
|
"github.com/yandex-cloud/go-genproto/yandex/cloud/iam/v1"
|
||||||
ycsdk "github.com/yandex-cloud/go-sdk"
|
ycsdk "github.com/yandex-cloud/go-sdk"
|
||||||
)
|
)
|
||||||
|
@ -28,6 +33,7 @@ import (
|
||||||
const (
|
const (
|
||||||
defaultStorageEndpoint = "storage.yandexcloud.net"
|
defaultStorageEndpoint = "storage.yandexcloud.net"
|
||||||
defaultStorageRegion = "ru-central1"
|
defaultStorageRegion = "ru-central1"
|
||||||
|
defaultSourceImageFamily = "ubuntu-1604-lts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -45,10 +51,25 @@ type Config struct {
|
||||||
|
|
||||||
// Path to a PEM encoded private key file to use to authenticate with SSH.
|
// Path to a PEM encoded private key file to use to authenticate with SSH.
|
||||||
// The `~` can be used in path and will be expanded to the home directory
|
// The `~` can be used in path and will be expanded to the home directory
|
||||||
// of current user. Login for attach: `ubuntu`
|
// of current user.
|
||||||
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file" required:"false"`
|
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file" required:"false"`
|
||||||
// Number of attempts to wait for export (must be greater than 0). Default: 1000
|
// The username to connect to SSH with. Default `ubuntu`
|
||||||
Tries int `mapstructure:"tries" required:"false"`
|
SSHUsername string `mapstructure:"ssh_username" required:"false"`
|
||||||
|
// The ID of the folder containing the source image. Default `standard-images`.
|
||||||
|
SourceImageFolderID string `mapstructure:"source_image_folder_id" required:"false"`
|
||||||
|
// The source image family to start export process. Default `ubuntu-1604-lts`.
|
||||||
|
// Image must contains utils or supported package manager: `apt` or `yum` -
|
||||||
|
// requires `root` or `sudo` without password.
|
||||||
|
// Utils: `qemu-img`, `aws`. The `qemu-img` utility requires `root` user or
|
||||||
|
// `sudo` access without password.
|
||||||
|
SourceImageFamily string `mapstructure:"source_image_family" required:"false"`
|
||||||
|
// The source image ID to use to create the new image from. Just one of a source_image_id or
|
||||||
|
// source_image_family must be specified.
|
||||||
|
SourceImageID string `mapstructure:"source_image_id" required:"false"`
|
||||||
|
// The extra size of the source disk in GB. This defaults to `0GB`.
|
||||||
|
// Requires `losetup` utility on the instance. Careful! Increases payment cost.
|
||||||
|
// See [perfomance](https://cloud.yandex.com/docs/compute/concepts/disk#performance).
|
||||||
|
SourceDiskExtraSize int `mapstructure:"source_disk_extra_size" required:"false"`
|
||||||
ctx interpolate.Context
|
ctx interpolate.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,8 +104,19 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||||
if p.config.DiskSizeGb == 0 {
|
if p.config.DiskSizeGb == 0 {
|
||||||
p.config.DiskSizeGb = 100
|
p.config.DiskSizeGb = 100
|
||||||
}
|
}
|
||||||
if p.config.Tries <= 0 {
|
if p.config.SSHUsername == "" {
|
||||||
p.config.Tries = 1000
|
p.config.SSHUsername = "ubuntu"
|
||||||
|
}
|
||||||
|
if p.config.SourceImageID == "" {
|
||||||
|
if p.config.SourceImageFamily == "" {
|
||||||
|
p.config.SourceImageFamily = defaultSourceImageFamily
|
||||||
|
}
|
||||||
|
if p.config.SourceImageFolderID == "" {
|
||||||
|
p.config.SourceImageFolderID = yandex.StandardImagesFolderID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.config.SourceDiskExtraSize < 0 {
|
||||||
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_disk_extra_size must be greater than zero"))
|
||||||
}
|
}
|
||||||
|
|
||||||
errs = p.config.CommonConfig.Prepare(errs)
|
errs = p.config.CommonConfig.Prepare(errs)
|
||||||
|
@ -131,12 +163,20 @@ func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifact packersdk.Artifact) (packersdk.Artifact, bool, bool, error) {
|
func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifact packersdk.Artifact) (packersdk.Artifact, bool, bool, error) {
|
||||||
|
imageID := ""
|
||||||
switch artifact.BuilderId() {
|
switch artifact.BuilderId() {
|
||||||
case yandex.BuilderID, artifice.BuilderId:
|
case yandex.BuilderID, artifice.BuilderId:
|
||||||
break
|
imageID = artifact.State("ImageID").(string)
|
||||||
|
case file.BuilderId:
|
||||||
|
fileName := artifact.Files()[0]
|
||||||
|
if content, err := ioutil.ReadFile(fileName); err == nil {
|
||||||
|
imageID = strings.TrimSpace(string(content))
|
||||||
|
} else {
|
||||||
|
return nil, false, false, err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"Unknown artifact type: %s\nCan only export from Yandex Cloud builder artifact or Artifice post-processor artifact.",
|
"Unknown artifact type: %s\nCan only export from Yandex Cloud builder artifact or File builder or Artifice post-processor artifact.",
|
||||||
artifact.BuilderId())
|
artifact.BuilderId())
|
||||||
return nil, false, false, err
|
return nil, false, false, err
|
||||||
}
|
}
|
||||||
|
@ -166,25 +206,23 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifa
|
||||||
|
|
||||||
log.Printf("Rendered path items: %v", p.config.Paths)
|
log.Printf("Rendered path items: %v", p.config.Paths)
|
||||||
|
|
||||||
imageID := artifact.State("ImageID").(string)
|
|
||||||
ui.Say(fmt.Sprintf("Exporting image %v to destination: %v", imageID, p.config.Paths))
|
ui.Say(fmt.Sprintf("Exporting image %v to destination: %v", imageID, p.config.Paths))
|
||||||
|
|
||||||
driver, err := yandex.NewDriverYC(ui, &p.config.AccessConfig)
|
driver, err := yandex.NewDriverYC(ui, &p.config.AccessConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, err
|
return nil, false, false, err
|
||||||
}
|
}
|
||||||
|
imageDescription, err := driver.SDK().Compute().Image().Get(ctx, &compute.GetImageRequest{
|
||||||
|
ImageId: imageID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, false, err
|
||||||
|
}
|
||||||
|
p.config.DiskConfig.DiskSizeGb = chooseBetterDiskSize(ctx, int(imageDescription.GetMinDiskSize()), p.config.DiskConfig.DiskSizeGb)
|
||||||
|
|
||||||
// Set up exporter instance configuration.
|
// Set up exporter instance configuration.
|
||||||
exporterName := fmt.Sprintf("%s-exporter", artifact.Id())
|
exporterName := strings.ToLower(fmt.Sprintf("%s-exporter", artifact.Id()))
|
||||||
yandexConfig := ycSaneDefaults(&p.config,
|
yandexConfig := ycSaneDefaults(&p.config, nil)
|
||||||
map[string]string{
|
|
||||||
"image_id": imageID,
|
|
||||||
"name": exporterName,
|
|
||||||
"paths": strings.Join(p.config.Paths, " "),
|
|
||||||
"user-data": CloudInitScript,
|
|
||||||
"zone": p.config.Zone,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if yandexConfig.InstanceConfig.InstanceName == "" {
|
if yandexConfig.InstanceConfig.InstanceName == "" {
|
||||||
yandexConfig.InstanceConfig.InstanceName = exporterName
|
yandexConfig.InstanceConfig.InstanceName = exporterName
|
||||||
}
|
}
|
||||||
|
@ -234,10 +272,16 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifa
|
||||||
&StepAttachDisk{
|
&StepAttachDisk{
|
||||||
CommonConfig: p.config.CommonConfig,
|
CommonConfig: p.config.CommonConfig,
|
||||||
ImageID: imageID,
|
ImageID: imageID,
|
||||||
|
ExtraSize: p.config.SourceDiskExtraSize,
|
||||||
},
|
},
|
||||||
new(StepUploadSecrets),
|
new(StepUploadSecrets),
|
||||||
&StepWaitCloudInitScript{
|
new(StepPrepareTools),
|
||||||
Tries: p.config.Tries,
|
&StepDump{
|
||||||
|
ExtraSize: p.config.SourceDiskExtraSize != 0,
|
||||||
|
SizeLimit: imageDescription.GetMinDiskSize(),
|
||||||
|
},
|
||||||
|
&StepUploadToS3{
|
||||||
|
Paths: p.config.Paths,
|
||||||
},
|
},
|
||||||
&yandex.StepTeardownInstance{
|
&yandex.StepTeardownInstance{
|
||||||
SerialLogFile: yandexConfig.SerialLogFile,
|
SerialLogFile: yandexConfig.SerialLogFile,
|
||||||
|
@ -267,13 +311,11 @@ func ycSaneDefaults(c *Config, md map[string]string) yandex.Config {
|
||||||
Communicator: communicator.Config{
|
Communicator: communicator.Config{
|
||||||
Type: "ssh",
|
Type: "ssh",
|
||||||
SSH: communicator.SSH{
|
SSH: communicator.SSH{
|
||||||
SSHUsername: "ubuntu",
|
SSHUsername: c.SSHUsername,
|
||||||
|
SSHPrivateKeyFile: c.SSHPrivateKeyFile,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if c.SSHPrivateKeyFile != "" {
|
|
||||||
yandexConfig.Communicator.SSH.SSHPrivateKeyFile = c.SSHPrivateKeyFile
|
|
||||||
}
|
|
||||||
if yandexConfig.Metadata == nil {
|
if yandexConfig.Metadata == nil {
|
||||||
yandexConfig.Metadata = md
|
yandexConfig.Metadata = md
|
||||||
} else {
|
} else {
|
||||||
|
@ -282,9 +324,9 @@ func ycSaneDefaults(c *Config, md map[string]string) yandex.Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yandexConfig.SourceImageFamily = "ubuntu-1604-lts"
|
yandexConfig.SourceImageFamily = c.SourceImageFamily
|
||||||
yandexConfig.SourceImageFolderID = yandex.StandardImagesFolderID
|
yandexConfig.SourceImageFolderID = c.SourceImageFolderID
|
||||||
|
yandexConfig.SourceImageID = c.SourceImageID
|
||||||
yandexConfig.ServiceAccountID = c.ServiceAccountID
|
yandexConfig.ServiceAccountID = c.ServiceAccountID
|
||||||
|
|
||||||
return yandexConfig
|
return yandexConfig
|
||||||
|
@ -305,3 +347,8 @@ func validateServiceAccount(ctx context.Context, ycsdk *ycsdk.SDK, serviceAccoun
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func chooseBetterDiskSize(ctx context.Context, minSizeBytes, oldSizeGB int) int {
|
||||||
|
max := math.Max(float64(minSizeBytes), float64((datasize.GB * datasize.ByteSize(oldSizeGB)).Bytes()))
|
||||||
|
return int(math.Ceil(datasize.ByteSize(max).GBytes()))
|
||||||
|
}
|
||||||
|
|
|
@ -46,7 +46,11 @@ type FlatConfig struct {
|
||||||
ServiceAccountID *string `mapstructure:"service_account_id" required:"true" cty:"service_account_id" hcl:"service_account_id"`
|
ServiceAccountID *string `mapstructure:"service_account_id" required:"true" cty:"service_account_id" hcl:"service_account_id"`
|
||||||
Paths []string `mapstructure:"paths" required:"true" cty:"paths" hcl:"paths"`
|
Paths []string `mapstructure:"paths" required:"true" cty:"paths" hcl:"paths"`
|
||||||
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" required:"false" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
|
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" required:"false" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
|
||||||
Tries *int `mapstructure:"tries" required:"false" cty:"tries" hcl:"tries"`
|
SSHUsername *string `mapstructure:"ssh_username" required:"false" cty:"ssh_username" hcl:"ssh_username"`
|
||||||
|
SourceImageFolderID *string `mapstructure:"source_image_folder_id" required:"false" cty:"source_image_folder_id" hcl:"source_image_folder_id"`
|
||||||
|
SourceImageFamily *string `mapstructure:"source_image_family" required:"false" cty:"source_image_family" hcl:"source_image_family"`
|
||||||
|
SourceImageID *string `mapstructure:"source_image_id" required:"false" cty:"source_image_id" hcl:"source_image_id"`
|
||||||
|
SourceDiskExtraSize *int `mapstructure:"source_disk_extra_size" required:"false" cty:"source_disk_extra_size" hcl:"source_disk_extra_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlatMapstructure returns a new FlatConfig.
|
// FlatMapstructure returns a new FlatConfig.
|
||||||
|
@ -97,7 +101,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
"service_account_id": &hcldec.AttrSpec{Name: "service_account_id", Type: cty.String, Required: false},
|
"service_account_id": &hcldec.AttrSpec{Name: "service_account_id", Type: cty.String, Required: false},
|
||||||
"paths": &hcldec.AttrSpec{Name: "paths", Type: cty.List(cty.String), Required: false},
|
"paths": &hcldec.AttrSpec{Name: "paths", Type: cty.List(cty.String), Required: false},
|
||||||
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
|
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
|
||||||
"tries": &hcldec.AttrSpec{Name: "tries", Type: cty.Number, Required: false},
|
"ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false},
|
||||||
|
"source_image_folder_id": &hcldec.AttrSpec{Name: "source_image_folder_id", Type: cty.String, Required: false},
|
||||||
|
"source_image_family": &hcldec.AttrSpec{Name: "source_image_family", Type: cty.String, Required: false},
|
||||||
|
"source_image_id": &hcldec.AttrSpec{Name: "source_image_id", Type: cty.String, Required: false},
|
||||||
|
"source_disk_extra_size": &hcldec.AttrSpec{Name: "source_disk_extra_size", Type: cty.Number, Required: false},
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
GetMetadata() {
|
|
||||||
curl -f -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/$1 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ "$(GetMetadata debug)" == "1" || "$(GetMetadata debug)" == "true" ]] && set -x
|
|
||||||
|
|
||||||
InstallPackages() {
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y qemu-utils awscli
|
|
||||||
}
|
|
||||||
|
|
||||||
WaitFile() {
|
|
||||||
local RETRIES=60
|
|
||||||
while [[ ${RETRIES} -gt 0 ]]; do
|
|
||||||
echo "Wait ${1}"
|
|
||||||
if [ -e "${1}" ]; then
|
|
||||||
echo "[${1}] has been found"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
RETRIES=$((RETRIES-1))
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
echo "[${1}] not found"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
PATHS=$(GetMetadata paths)
|
|
||||||
S3_ENDPOINT="https://storage.yandexcloud.net"
|
|
||||||
DISK_EXPORT_PATH="/dev/disk/by-id/virtio-doexport"
|
|
||||||
export AWS_SHARED_CREDENTIALS_FILE="/tmp/aws-credentials"
|
|
||||||
export AWS_REGION=ru-central1
|
|
||||||
|
|
||||||
Exit() {
|
|
||||||
for i in ${PATHS}; do
|
|
||||||
LOGDEST="${i}.exporter.log"
|
|
||||||
echo "Uploading exporter log to ${LOGDEST}..."
|
|
||||||
aws s3 --endpoint-url="${S3_ENDPOINT}" cp /var/log/syslog "${LOGDEST}"
|
|
||||||
done
|
|
||||||
|
|
||||||
exit $1
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallPackages
|
|
||||||
|
|
||||||
echo "####### Export configuration #######"
|
|
||||||
echo "Export paths - ${PATHS}"
|
|
||||||
echo "####################################"
|
|
||||||
|
|
||||||
if ! WaitFile "${AWS_SHARED_CREDENTIALS_FILE}"; then
|
|
||||||
echo "Failed wait credentials"
|
|
||||||
Exit 1
|
|
||||||
fi
|
|
||||||
udevadm trigger || true
|
|
||||||
|
|
||||||
if ! WaitFile "${DISK_EXPORT_PATH}"; then
|
|
||||||
echo "Failed wait attach disk"
|
|
||||||
Exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Dumping disk..."
|
|
||||||
if ! qemu-img convert -O qcow2 -o cluster_size=2M "${DISK_EXPORT_PATH}" disk.qcow2; then
|
|
||||||
echo "Failed to dump disk to qcow2 image."
|
|
||||||
Exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in ${PATHS}; do
|
|
||||||
echo "Uploading qcow2 disk image to ${i}..."
|
|
||||||
if ! aws s3 --endpoint-url="${S3_ENDPOINT}" cp disk.qcow2 "${i}"; then
|
|
||||||
echo "Failed to upload image to ${i}."
|
|
||||||
FAIL=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
Exit ${FAIL}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"golang.org/x/tools/imports"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
tmpl = template.Must(template.New("var").Parse(`
|
|
||||||
// CODE GENERATED. DO NOT EDIT
|
|
||||||
package {{.PkgName }}
|
|
||||||
var (
|
|
||||||
{{ .Name }} = ` + "`" + `{{.Value}}` + "`" + `
|
|
||||||
)
|
|
||||||
|
|
||||||
`))
|
|
||||||
)
|
|
||||||
|
|
||||||
type vars struct {
|
|
||||||
PkgName string
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
||||||
if len(os.Args) < 3 {
|
|
||||||
log.Fatalf("Usage: %s file varname [output]", os.Args[0])
|
|
||||||
}
|
|
||||||
fname := os.Args[1]
|
|
||||||
targetVar := os.Args[2]
|
|
||||||
pkg := os.Getenv("GOPACKAGE")
|
|
||||||
absFilePath, err := filepath.Abs(fname)
|
|
||||||
|
|
||||||
targetFName := strings.ToLower(targetVar) + ".go"
|
|
||||||
if len(os.Args) > 3 {
|
|
||||||
targetFName = os.Args[3]
|
|
||||||
}
|
|
||||||
log.Println(absFilePath, "=>", targetFName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
b, err := ioutil.ReadFile(fname)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(absFilePath); err != nil {
|
|
||||||
os.Remove(absFilePath)
|
|
||||||
}
|
|
||||||
buff := bytes.Buffer{}
|
|
||||||
err = tmpl.Execute(&buff, vars{
|
|
||||||
Name: targetVar,
|
|
||||||
Value: string(b),
|
|
||||||
PkgName: pkg,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := imports.Process(targetFName, buff.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
f, err := os.Create(targetFName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = f.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
"github.com/hashicorp/packer/builder/yandex"
|
"github.com/hashicorp/packer/builder/yandex"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
type StepAttachDisk struct {
|
type StepAttachDisk struct {
|
||||||
yandex.CommonConfig
|
yandex.CommonConfig
|
||||||
ImageID string
|
ImageID string
|
||||||
|
ExtraSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *StepAttachDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
func (c *StepAttachDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
@ -34,7 +36,7 @@ func (c *StepAttachDisk) Run(ctx context.Context, state multistep.StateBag) mult
|
||||||
ImageId: c.ImageID,
|
ImageId: c.ImageID,
|
||||||
},
|
},
|
||||||
Name: fmt.Sprintf("export-%s-disk", instanceID),
|
Name: fmt.Sprintf("export-%s-disk", instanceID),
|
||||||
Size: imageDesc.GetMinDiskSize(),
|
Size: int64(datasize.ByteSize(c.ExtraSize)*datasize.GB) + imageDesc.GetMinDiskSize(),
|
||||||
ZoneId: c.Zone,
|
ZoneId: c.Zone,
|
||||||
FolderId: c.FolderID,
|
FolderId: c.FolderID,
|
||||||
TypeId: c.DiskType,
|
TypeId: c.DiskType,
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package yandexexport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer/builder/yandex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepDump struct {
|
||||||
|
ExtraSize bool
|
||||||
|
SizeLimit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
dumpCommand = "%sqemu-img convert -O qcow2 -o cluster_size=2M %s disk.qcow2 2>&1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run reads the instance metadata and looks for the log entry
|
||||||
|
// indicating the cloud-init script finished.
|
||||||
|
func (s *StepDump) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
comm := state.Get("communicator").(packersdk.Communicator)
|
||||||
|
|
||||||
|
device := "/dev/disk/by-id/virtio-doexport"
|
||||||
|
cmdDumpCheckAccess := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf("qemu-img info %s", device),
|
||||||
|
}
|
||||||
|
if err := comm.Start(ctx, cmdDumpCheckAccess); err != nil {
|
||||||
|
return yandex.StepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
sudo := ""
|
||||||
|
if cmdDumpCheckAccess.Wait() != 0 {
|
||||||
|
sudo = "sudo "
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ExtraSize && which(ctx, comm, "losetup") == nil {
|
||||||
|
ui.Say("Map loop device...")
|
||||||
|
buff := new(bytes.Buffer)
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf("%slosetup --show -r --sizelimit %d -f %s", sudo, s.SizeLimit, device),
|
||||||
|
Stdout: buff,
|
||||||
|
}
|
||||||
|
if err := comm.Start(ctx, cmd); err != nil {
|
||||||
|
return yandex.StepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
if cmd.Wait() != 0 {
|
||||||
|
return yandex.StepHaltWithError(state, fmt.Errorf("Cannot losetup: %d", cmd.ExitStatus()))
|
||||||
|
}
|
||||||
|
device = strings.TrimSpace(buff.String())
|
||||||
|
if device == "" {
|
||||||
|
return yandex.StepHaltWithError(state, fmt.Errorf("Bad lo device"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
defer wg.Wait()
|
||||||
|
ctxWithCancel, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
wg.Add(1)
|
||||||
|
defer wg.Done()
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: "while true ; do sleep 3; sudo kill -s SIGUSR1 $(pidof qemu-img); done",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.RunWithUi(ctxWithCancel, comm, ui)
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
ui.Error("qemu-img signal sender error: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cmdDump := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf(dumpCommand, sudo, device),
|
||||||
|
}
|
||||||
|
ui.Say("Dumping...")
|
||||||
|
if err := cmdDump.RunWithUi(ctx, comm, ui); err != nil {
|
||||||
|
return yandex.StepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
if cmdDump.ExitStatus() != 0 {
|
||||||
|
return yandex.StepHaltWithError(state, fmt.Errorf("Cannot dump disk, exit code: %d", cmdDump.ExitStatus()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup nothing
|
||||||
|
func (s *StepDump) Cleanup(state multistep.StateBag) {}
|
|
@ -0,0 +1,150 @@
|
||||||
|
package yandexexport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer/builder/yandex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepPrepareTools struct{}
|
||||||
|
|
||||||
|
// Run reads the instance metadata and looks for the log entry
|
||||||
|
// indicating the cloud-init script finished.
|
||||||
|
func (s *StepPrepareTools) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
comm := state.Get("communicator").(packersdk.Communicator)
|
||||||
|
pkgManager, errPkgManager := detectPkgManager(ctx, comm)
|
||||||
|
|
||||||
|
if which(ctx, comm, "qemu-img") != nil {
|
||||||
|
if errPkgManager != nil {
|
||||||
|
return yandex.StepHaltWithError(state, errPkgManager)
|
||||||
|
}
|
||||||
|
ui.Message("Install qemu-img...")
|
||||||
|
if err := pkgManager.InstallQemuIMG(ctx, comm); err != nil {
|
||||||
|
return yandex.StepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if which(ctx, comm, "aws") != nil {
|
||||||
|
if errPkgManager != nil {
|
||||||
|
return yandex.StepHaltWithError(state, errPkgManager)
|
||||||
|
}
|
||||||
|
ui.Message("Install aws...")
|
||||||
|
if err := pkgManager.InstallAWS(ctx, comm); err != nil {
|
||||||
|
return yandex.StepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup nothing
|
||||||
|
func (s *StepPrepareTools) Cleanup(state multistep.StateBag) {}
|
||||||
|
|
||||||
|
func detectPkgManager(ctx context.Context, comm packersdk.Communicator) (pkgManager, error) {
|
||||||
|
if err := which(ctx, comm, "apt"); err == nil {
|
||||||
|
return &apt{}, nil
|
||||||
|
}
|
||||||
|
if err := which(ctx, comm, "yum"); err == nil {
|
||||||
|
return &yum{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Cannot detect package manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
func which(ctx context.Context, comm packersdk.Communicator, what string) error {
|
||||||
|
cmdCheckAPT := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf("which %s", what),
|
||||||
|
}
|
||||||
|
if err := comm.Start(ctx, cmdCheckAPT); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmdCheckAPT.Wait() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Not found: %s", what)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pkgManager interface {
|
||||||
|
InstallQemuIMG(ctx context.Context, comm packersdk.Communicator) error
|
||||||
|
InstallAWS(ctx context.Context, comm packersdk.Communicator) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type apt struct {
|
||||||
|
updated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *apt) InstallAWS(ctx context.Context, comm packersdk.Communicator) error {
|
||||||
|
if err := p.Update(ctx, comm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := execCMDWithSudo(ctx, comm, "apt install -y awscli"); err != nil {
|
||||||
|
return fmt.Errorf("Cannot install awscli")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *apt) InstallQemuIMG(ctx context.Context, comm packersdk.Communicator) error {
|
||||||
|
if err := p.Update(ctx, comm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := execCMDWithSudo(ctx, comm, "apt install -y qemu-utils"); err != nil {
|
||||||
|
return fmt.Errorf("Cannot install qemu-utils")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *apt) Update(ctx context.Context, comm packersdk.Communicator) error {
|
||||||
|
if p.updated {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := execCMDWithSudo(ctx, comm, "apt update"); err != nil {
|
||||||
|
return fmt.Errorf("Cannot update: %s", err)
|
||||||
|
}
|
||||||
|
p.updated = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type yum struct{}
|
||||||
|
|
||||||
|
func (p *yum) InstallAWS(ctx context.Context, comm packersdk.Communicator) error {
|
||||||
|
if which(ctx, comm, "pip3") != nil {
|
||||||
|
if err := execCMDWithSudo(ctx, comm, "yum install -y python3-pip"); err != nil {
|
||||||
|
return fmt.Errorf("Cannot install qemu-img: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := execCMDWithSudo(ctx, comm, "pip3 install awscli"); err != nil {
|
||||||
|
return fmt.Errorf("Install awscli: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *yum) InstallQemuIMG(ctx context.Context, comm packersdk.Communicator) error {
|
||||||
|
if err := execCMDWithSudo(ctx, comm, "yum install -y libgcrypt qemu-img"); err != nil {
|
||||||
|
return fmt.Errorf("Cannot install qemu-img: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func execCMDWithSudo(ctx context.Context, comm packersdk.Communicator, cmdStr string) error {
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: cmdStr,
|
||||||
|
}
|
||||||
|
if err := comm.Start(ctx, cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.Wait() != 0 {
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf("sudo %s", cmdStr),
|
||||||
|
}
|
||||||
|
if err := comm.Start(ctx, cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.Wait() != 0 {
|
||||||
|
return fmt.Errorf("Bad exit code: %d", cmd.ExitStatus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -13,6 +13,10 @@ import (
|
||||||
|
|
||||||
type StepUploadSecrets struct{}
|
type StepUploadSecrets struct{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sharedAWSCredFile = "/tmp/aws-credentials"
|
||||||
|
)
|
||||||
|
|
||||||
// Run reads the instance metadata and looks for the log entry
|
// Run reads the instance metadata and looks for the log entry
|
||||||
// indicating the cloud-init script finished.
|
// indicating the cloud-init script finished.
|
||||||
func (s *StepUploadSecrets) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
func (s *StepUploadSecrets) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
@ -28,7 +32,7 @@ func (s *StepUploadSecrets) Run(ctx context.Context, state multistep.StateBag) m
|
||||||
s3Secret.GetAccessKey().GetKeyId(),
|
s3Secret.GetAccessKey().GetKeyId(),
|
||||||
s3Secret.GetSecret())
|
s3Secret.GetSecret())
|
||||||
|
|
||||||
err := comm.Upload("/tmp/aws-credentials", strings.NewReader(creds), nil)
|
err := comm.Upload(sharedAWSCredFile, strings.NewReader(creds), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return yandex.StepHaltWithError(state, err)
|
return yandex.StepHaltWithError(state, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package yandexexport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer/builder/yandex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepUploadToS3 struct {
|
||||||
|
Paths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run reads the instance metadata and looks for the log entry
|
||||||
|
// indicating the cloud-init script finished.
|
||||||
|
func (s *StepUploadToS3) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
comm := state.Get("communicator").(packersdk.Communicator)
|
||||||
|
|
||||||
|
cmdUploadToS3 := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf(
|
||||||
|
"%s=%s aws s3 --region=%s --endpoint-url=https://%s cp disk.qcow2 %s",
|
||||||
|
"AWS_SHARED_CREDENTIALS_FILE",
|
||||||
|
sharedAWSCredFile,
|
||||||
|
defaultStorageRegion,
|
||||||
|
defaultStorageEndpoint,
|
||||||
|
s.Paths[0],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ui.Say("Upload to S3...")
|
||||||
|
if err := cmdUploadToS3.RunWithUi(ctx, comm, ui); err != nil {
|
||||||
|
return yandex.StepHaltWithError(state, err)
|
||||||
|
}
|
||||||
|
if cmdUploadToS3.ExitStatus() != 0 {
|
||||||
|
return yandex.StepHaltWithError(state, fmt.Errorf("Cannout upload to S3, exit code %d", cmdUploadToS3.ExitStatus()))
|
||||||
|
}
|
||||||
|
|
||||||
|
versionExtraFlags, err := getVersionExtraFlags(ctx, comm)
|
||||||
|
if err != nil {
|
||||||
|
ui.Message(fmt.Sprintf("[WARN] Cannot upload to other storage: %s", err))
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
defer wg.Wait()
|
||||||
|
for _, path := range s.Paths[1:] {
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(path string) {
|
||||||
|
defer wg.Done()
|
||||||
|
ui.Message(fmt.Sprintf("Start copy %s to %s...", s.Paths[0], path))
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf(
|
||||||
|
"%s=%s aws s3 --region=%s --endpoint-url=https://%s cp %s %s %s",
|
||||||
|
"AWS_SHARED_CREDENTIALS_FILE",
|
||||||
|
sharedAWSCredFile,
|
||||||
|
defaultStorageRegion,
|
||||||
|
defaultStorageEndpoint,
|
||||||
|
versionExtraFlags,
|
||||||
|
s.Paths[0],
|
||||||
|
path,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
|
||||||
|
ui.Message(fmt.Sprintf("[WARN] Failed upload to %s", path))
|
||||||
|
}
|
||||||
|
if cmd.ExitStatus() != 0 {
|
||||||
|
ui.Message(fmt.Sprintf("[WARN] Failed upload to %s", path))
|
||||||
|
}
|
||||||
|
}(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup nothing
|
||||||
|
func (s *StepUploadToS3) Cleanup(state multistep.StateBag) {}
|
||||||
|
|
||||||
|
func getVersionExtraFlags(ctx context.Context, comm packersdk.Communicator) (string, error) {
|
||||||
|
buff := new(bytes.Buffer)
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: "aws --version",
|
||||||
|
Stdout: buff,
|
||||||
|
}
|
||||||
|
if err := comm.Start(ctx, cmd); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if cmd.Wait() != 0 {
|
||||||
|
return "", fmt.Errorf("Cannot detect aws version")
|
||||||
|
}
|
||||||
|
vsn := buff.String()
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(vsn, "aws-cli/2."):
|
||||||
|
return "--copy-props metadata-directive", nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
|
@ -1,116 +0,0 @@
|
||||||
package yandexexport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
|
||||||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
||||||
"github.com/hashicorp/packer-plugin-sdk/retry"
|
|
||||||
"github.com/hashicorp/packer/builder/yandex"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StepWaitCloudInitScript struct {
|
|
||||||
Tries int
|
|
||||||
}
|
|
||||||
|
|
||||||
type cloudInitStatus struct {
|
|
||||||
V1 struct {
|
|
||||||
Errors []interface{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type cloudInitError struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *cloudInitError) Error() string {
|
|
||||||
return e.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run reads the instance metadata and looks for the log entry
|
|
||||||
// indicating the cloud-init script finished.
|
|
||||||
func (s *StepWaitCloudInitScript) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
||||||
ui := state.Get("ui").(packersdk.Ui)
|
|
||||||
comm := state.Get("communicator").(packersdk.Communicator)
|
|
||||||
|
|
||||||
ui.Say("Waiting for any running cloud-init script to finish...")
|
|
||||||
|
|
||||||
ctxWithCancel, cancelCtx := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
defer cancelCtx()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
cmd := &packersdk.RemoteCmd{
|
|
||||||
Command: "tail -f /var/log/cloud-init-output.log",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := cmd.RunWithUi(ctxWithCancel, comm, ui)
|
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
ui.Error(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ui.Message("Cloud-init output closed")
|
|
||||||
}()
|
|
||||||
|
|
||||||
// periodically show progress by sending SIGUSR1 to `qemu-img` process
|
|
||||||
go func() {
|
|
||||||
cmd := &packersdk.RemoteCmd{
|
|
||||||
Command: "until pid=$(pidof qemu-img) ; do sleep 1 ; done ; " +
|
|
||||||
"while true ; do sudo kill -s SIGUSR1 ${pid}; sleep 10 ; done",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := cmd.RunWithUi(ctxWithCancel, comm, ui)
|
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
ui.Error("qemu-img signal sender error: " + err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Keep checking the serial port output to see if the cloud-init script is done.
|
|
||||||
retryConfig := &retry.Config{
|
|
||||||
ShouldRetry: func(e error) bool {
|
|
||||||
switch e.(type) {
|
|
||||||
case *cloudInitError:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
Tries: s.Tries,
|
|
||||||
RetryDelay: (&retry.Backoff{InitialBackoff: 10 * time.Second, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := retryConfig.Run(ctx, func(ctx context.Context) error {
|
|
||||||
buff := bytes.Buffer{}
|
|
||||||
err := comm.Download("/var/run/cloud-init/result.json", &buff)
|
|
||||||
if err != nil {
|
|
||||||
err := fmt.Errorf("Waiting cloud-init script status: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result := &cloudInitStatus{}
|
|
||||||
err = json.Unmarshal(buff.Bytes(), result)
|
|
||||||
if err != nil {
|
|
||||||
err := fmt.Errorf("Failed parse result: %s", err)
|
|
||||||
return &cloudInitError{Err: err}
|
|
||||||
}
|
|
||||||
if len(result.V1.Errors) != 0 {
|
|
||||||
err := fmt.Errorf("Result: %v", result.V1.Errors)
|
|
||||||
return &cloudInitError{Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
err := fmt.Errorf("Error waiting for cloud-init script to finish: %s", err)
|
|
||||||
return yandex.StepHaltWithError(state, err)
|
|
||||||
}
|
|
||||||
ui.Say("Cloud-init script has finished running.")
|
|
||||||
return multistep.ActionContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup.
|
|
||||||
func (s *StepWaitCloudInitScript) Cleanup(state multistep.StateBag) {}
|
|
|
@ -80,6 +80,8 @@ can also be supplied to override the typical auto-generated key:
|
||||||
|
|
||||||
@include 'builder/yandex/Config-required.mdx'
|
@include 'builder/yandex/Config-required.mdx'
|
||||||
|
|
||||||
|
@include 'builder/yandex/SourceImageConfig-required.mdx'
|
||||||
|
|
||||||
#### Common
|
#### Common
|
||||||
|
|
||||||
@include 'builder/yandex/CloudConfig-required.mdx'
|
@include 'builder/yandex/CloudConfig-required.mdx'
|
||||||
|
@ -94,6 +96,8 @@ can also be supplied to override the typical auto-generated key:
|
||||||
|
|
||||||
@include 'builder/yandex/Config-not-required.mdx'
|
@include 'builder/yandex/Config-not-required.mdx'
|
||||||
|
|
||||||
|
@include 'builder/yandex/SourceImageConfig-not-required.mdx'
|
||||||
|
|
||||||
#### Common
|
#### Common
|
||||||
|
|
||||||
@include 'builder/yandex/CommonConfig-not-required.mdx'
|
@include 'builder/yandex/CommonConfig-not-required.mdx'
|
||||||
|
|
|
@ -2,12 +2,5 @@
|
||||||
|
|
||||||
- `service_account_id` (string) - Service account identifier to assign to instance.
|
- `service_account_id` (string) - Service account identifier to assign to instance.
|
||||||
|
|
||||||
- `source_image_folder_id` (string) - The ID of the folder containing the source image.
|
|
||||||
|
|
||||||
- `source_image_id` (string) - The source image ID to use to create the new image from.
|
|
||||||
|
|
||||||
- `source_image_name` (string) - The source image name to use to create the new image
|
|
||||||
from. Name will be looked up in `source_image_folder_id`.
|
|
||||||
|
|
||||||
- `target_image_folder_id` (string) - The ID of the folder to save built image in.
|
- `target_image_folder_id` (string) - The ID of the folder to save built image in.
|
||||||
This defaults to value of 'folder_id'.
|
This defaults to value of 'folder_id'.
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<!-- Code generated from the comments of the SourceImageConfig struct in builder/yandex/common_config.go; DO NOT EDIT MANUALLY -->
|
||||||
|
|
||||||
|
- `source_image_folder_id` (string) - The ID of the folder containing the source image.
|
||||||
|
|
||||||
|
- `source_image_id` (string) - The source image ID to use to create the new image from.
|
||||||
|
|
||||||
|
- `source_image_name` (string) - The source image name to use to create the new image
|
||||||
|
from. Name will be looked up in `source_image_folder_id`.
|
|
@ -0,0 +1,5 @@
|
||||||
|
<!-- Code generated from the comments of the SourceImageConfig struct in builder/yandex/common_config.go; DO NOT EDIT MANUALLY -->
|
||||||
|
|
||||||
|
- `source_image_family` (string) - The source image family to create the new image
|
||||||
|
from. You can also specify source_image_id instead. Just one of a source_image_id or
|
||||||
|
source_image_family must be specified. Example: `ubuntu-1804-lts`.
|
|
@ -2,6 +2,21 @@
|
||||||
|
|
||||||
- `ssh_private_key_file` (string) - Path to a PEM encoded private key file to use to authenticate with SSH.
|
- `ssh_private_key_file` (string) - Path to a PEM encoded private key file to use to authenticate with SSH.
|
||||||
The `~` can be used in path and will be expanded to the home directory
|
The `~` can be used in path and will be expanded to the home directory
|
||||||
of current user. Login for attach: `ubuntu`
|
of current user.
|
||||||
|
|
||||||
- `tries` (int) - Number of attempts to wait for export (must be greater than 0). Default: 1000
|
- `ssh_username` (string) - The username to connect to SSH with. Default `ubuntu`
|
||||||
|
|
||||||
|
- `source_image_folder_id` (string) - The ID of the folder containing the source image. Default `standard-images`.
|
||||||
|
|
||||||
|
- `source_image_family` (string) - The source image family to start export process. Default `ubuntu-1604-lts`.
|
||||||
|
Image must contains utils or supported package manager: `apt` or `yum` -
|
||||||
|
requires `root` or `sudo` without password.
|
||||||
|
Utils: `qemu-img`, `aws`. The `qemu-img` utility requires `root` user or
|
||||||
|
`sudo` access without password.
|
||||||
|
|
||||||
|
- `source_image_id` (string) - The source image ID to use to create the new image from. Just one of a source_image_id or
|
||||||
|
source_image_family must be specified.
|
||||||
|
|
||||||
|
- `source_disk_extra_size` (int) - The extra size of the source disk in GB. This defaults to `0GB`.
|
||||||
|
Requires `losetup` utility on the instance. Careful! Increases payment cost.
|
||||||
|
See [perfomance](https://cloud.yandex.com/docs/compute/concepts/disk#performance).
|
||||||
|
|
Loading…
Reference in New Issue