2019-05-31 08:27:41 -04:00
|
|
|
//go:generate struct-markdown
|
2019-10-14 10:43:59 -04:00
|
|
|
//go:generate mapstructure-to-hcl2 -type Config
|
2019-05-31 08:27:41 -04:00
|
|
|
|
2016-01-11 06:22:41 -05:00
|
|
|
package cloudstack
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
|
2020-12-17 16:29:25 -05:00
|
|
|
"github.com/hashicorp/packer-plugin-sdk/common"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
|
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
|
|
|
"github.com/hashicorp/packer-plugin-sdk/uuid"
|
2016-01-11 06:22:41 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// Config holds all the details needed to configure the builder.
|
|
|
|
type Config struct {
|
2020-11-11 18:04:28 -05:00
|
|
|
common.PackerConfig `mapstructure:",squash"`
|
|
|
|
commonsteps.HTTPConfig `mapstructure:",squash"`
|
|
|
|
Comm communicator.Config `mapstructure:",squash"`
|
2016-01-11 06:22:41 -05:00
|
|
|
|
2019-05-28 11:50:58 -04:00
|
|
|
// The CloudStack API endpoint we will connect to. It can
|
2019-06-06 10:29:25 -04:00
|
|
|
// also be specified via environment variable CLOUDSTACK_API_URL, if set.
|
|
|
|
APIURL string `mapstructure:"api_url" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The API key used to sign all API requests. It can also
|
2019-06-06 10:29:25 -04:00
|
|
|
// be specified via environment variable CLOUDSTACK_API_KEY, if set.
|
|
|
|
APIKey string `mapstructure:"api_key" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The secret key used to sign all API requests. It
|
2019-06-06 10:29:25 -04:00
|
|
|
// can also be specified via environment variable CLOUDSTACK_SECRET_KEY, if
|
|
|
|
// set.
|
|
|
|
SecretKey string `mapstructure:"secret_key" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The time duration to wait for async calls to
|
2019-06-06 10:29:25 -04:00
|
|
|
// finish. Defaults to 30m.
|
2019-05-28 11:50:58 -04:00
|
|
|
AsyncTimeout time.Duration `mapstructure:"async_timeout" required:"false"`
|
|
|
|
// Some cloud providers only allow HTTP GET calls
|
2019-06-06 10:29:25 -04:00
|
|
|
// to their CloudStack API. If using such a provider, you need to set this to
|
|
|
|
// true in order for the provider to only make GET calls and no POST calls.
|
|
|
|
HTTPGetOnly bool `mapstructure:"http_get_only" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to skip SSL verification.
|
2019-06-06 10:29:25 -04:00
|
|
|
// Defaults to false.
|
|
|
|
SSLNoVerify bool `mapstructure:"ssl_no_verify" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// List of CIDR's that will have access to the new
|
2019-06-06 10:29:25 -04:00
|
|
|
// instance. This is needed in order for any provisioners to be able to
|
|
|
|
// connect to the instance. Defaults to [ "0.0.0.0/0" ]. Only required when
|
|
|
|
// use_local_ip_address is false.
|
|
|
|
CIDRList []string `mapstructure:"cidr_list" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// If true a temporary security group
|
2019-06-06 10:29:25 -04:00
|
|
|
// will be created which allows traffic towards the instance from the
|
|
|
|
// cidr_list. This option will be ignored if security_groups is also
|
|
|
|
// defined. Requires expunge set to true. Defaults to false.
|
|
|
|
CreateSecurityGroup bool `mapstructure:"create_security_group" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the disk offering used for the
|
2019-06-06 10:29:25 -04:00
|
|
|
// instance. This option is only available (and also required) when using
|
|
|
|
// source_iso.
|
|
|
|
DiskOffering string `mapstructure:"disk_offering" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The size (in GB) of the root disk of the new
|
2019-06-06 10:29:25 -04:00
|
|
|
// instance. This option is only available when using source_template.
|
|
|
|
DiskSize int64 `mapstructure:"disk_size" required:"false"`
|
2019-09-20 09:04:01 -04:00
|
|
|
// If `true` make a call to the CloudStack API, after loading image to
|
|
|
|
// cache, requesting to check and detach ISO file (if any) currently
|
|
|
|
// attached to a virtual machine. Defaults to `false`. This option is only
|
|
|
|
// available when using `source_iso`.
|
2019-09-20 04:54:38 -04:00
|
|
|
EjectISO bool `mapstructure:"eject_iso"`
|
2019-09-20 09:04:01 -04:00
|
|
|
// Configure the duration time to wait, making sure virtual machine is able
|
|
|
|
// to finish installing OS before it ejects safely. Requires `eject_iso`
|
|
|
|
// set to `true` and this option is only available when using `source_iso`.
|
2019-09-12 19:35:14 -04:00
|
|
|
EjectISODelay time.Duration `mapstructure:"eject_iso_delay"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to expunge the instance when it is
|
2019-06-06 10:29:25 -04:00
|
|
|
// destroyed. Defaults to false.
|
|
|
|
Expunge bool `mapstructure:"expunge" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The target hypervisor (e.g. XenServer, KVM) for
|
2019-06-06 10:29:25 -04:00
|
|
|
// the new template. This option is required when using source_iso.
|
|
|
|
Hypervisor string `mapstructure:"hypervisor" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name of the instance. Defaults to
|
2019-06-06 10:29:25 -04:00
|
|
|
// "packer-UUID" where UUID is dynamically generated.
|
|
|
|
InstanceName string `mapstructure:"instance_name" required:"false"`
|
2019-10-27 00:13:45 -04:00
|
|
|
// The display name of the instance. Defaults to "Created by Packer".
|
|
|
|
InstanceDisplayName string `mapstructure:"instance_display_name" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the network to connect the instance
|
2019-06-06 10:29:25 -04:00
|
|
|
// to.
|
|
|
|
Network string `mapstructure:"network" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the project to deploy the instance
|
2019-06-06 10:29:25 -04:00
|
|
|
// to.
|
|
|
|
Project string `mapstructure:"project" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The public IP address or it's ID used for
|
2019-06-06 10:29:25 -04:00
|
|
|
// connecting any provisioners to. If not provided, a temporary public IP
|
|
|
|
// address will be associated and released during the Packer run.
|
|
|
|
PublicIPAddress string `mapstructure:"public_ip_address" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The fixed port you want to configure in the port
|
2019-06-06 10:29:25 -04:00
|
|
|
// forwarding rule. Set this attribute if you do not want to use the a random
|
|
|
|
// public port.
|
|
|
|
PublicPort int `mapstructure:"public_port" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// A list of security group IDs or
|
2019-06-06 10:29:25 -04:00
|
|
|
// names to associate the instance with.
|
|
|
|
SecurityGroups []string `mapstructure:"security_groups" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the service offering used
|
2019-06-06 10:29:25 -04:00
|
|
|
// for the instance.
|
|
|
|
ServiceOffering string `mapstructure:"service_offering" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to prevent network
|
2019-06-06 10:29:25 -04:00
|
|
|
// ACLs or firewall rules creation. Defaults to false.
|
|
|
|
PreventFirewallChanges bool `mapstructure:"prevent_firewall_changes" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of an ISO that will be mounted
|
2019-06-06 10:29:25 -04:00
|
|
|
// before booting the instance. This option is mutually exclusive with
|
|
|
|
// source_template. When using source_iso, both disk_offering and
|
|
|
|
// hypervisor are required.
|
|
|
|
SourceISO string `mapstructure:"source_iso" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the template used as base
|
2019-06-06 10:29:25 -04:00
|
|
|
// template for the instance. This option is mutually exclusive with
|
|
|
|
// source_iso.
|
|
|
|
SourceTemplate string `mapstructure:"source_template" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name of the temporary SSH key pair
|
2019-06-06 10:29:25 -04:00
|
|
|
// to generate. By default, Packer generates a name that looks like
|
2020-04-01 23:30:51 -04:00
|
|
|
// `packer_<UUID>`, where `<UUID>` is a 36 character unique identifier.
|
2019-06-06 10:29:25 -04:00
|
|
|
TemporaryKeypairName string `mapstructure:"temporary_keypair_name" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to indicate that the
|
2019-06-06 10:29:25 -04:00
|
|
|
// provisioners should connect to the local IP address of the instance.
|
|
|
|
UseLocalIPAddress bool `mapstructure:"use_local_ip_address" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// User data to launch with the instance. This is a
|
2021-02-02 12:05:04 -05:00
|
|
|
// template engine; see "User Data" below for
|
2019-06-06 10:29:25 -04:00
|
|
|
// more details. Packer will not automatically wait for a user script to
|
|
|
|
// finish before shutting down the instance this must be handled in a
|
|
|
|
// provisioner.
|
|
|
|
UserData string `mapstructure:"user_data" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Path to a file that will be used for the user
|
2019-06-06 10:29:25 -04:00
|
|
|
// data when launching the instance. This file will be parsed as a template
|
2021-02-02 12:05:04 -05:00
|
|
|
// engine see User Data below for more
|
2019-06-06 10:29:25 -04:00
|
|
|
// details.
|
|
|
|
UserDataFile string `mapstructure:"user_data_file" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the zone where the instance will be
|
2019-06-06 10:29:25 -04:00
|
|
|
// created.
|
|
|
|
Zone string `mapstructure:"zone" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name of the new template. Defaults to
|
2020-03-23 20:02:12 -04:00
|
|
|
// `packer-{{timestamp}}` where timestamp will be the current time.
|
2019-06-06 10:29:25 -04:00
|
|
|
TemplateName string `mapstructure:"template_name" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The display text of the new template.
|
2019-06-06 10:29:25 -04:00
|
|
|
// Defaults to the template_name.
|
|
|
|
TemplateDisplayText string `mapstructure:"template_display_text" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// The name or ID of the template OS for the new
|
2019-06-06 10:29:25 -04:00
|
|
|
// template that will be created.
|
|
|
|
TemplateOS string `mapstructure:"template_os" required:"true"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to indicate that the template
|
2019-06-06 10:29:25 -04:00
|
|
|
// is featured. Defaults to false.
|
|
|
|
TemplateFeatured bool `mapstructure:"template_featured" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to indicate that the template
|
2019-06-06 10:29:25 -04:00
|
|
|
// is available for all accounts. Defaults to false.
|
|
|
|
TemplatePublic bool `mapstructure:"template_public" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to indicate the
|
2019-06-06 10:29:25 -04:00
|
|
|
// template should be password enabled. Defaults to false.
|
|
|
|
TemplatePasswordEnabled bool `mapstructure:"template_password_enabled" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to indicate the template
|
2019-06-06 10:29:25 -04:00
|
|
|
// requires hardware-assisted virtualization. Defaults to false.
|
|
|
|
TemplateRequiresHVM bool `mapstructure:"template_requires_hvm" required:"false"`
|
2019-05-28 11:50:58 -04:00
|
|
|
// Set to true to indicate that the template
|
2019-06-06 10:29:25 -04:00
|
|
|
// contains tools to support dynamic scaling of VM cpu/memory. Defaults to
|
|
|
|
// false.
|
2019-09-20 04:54:38 -04:00
|
|
|
TemplateScalable bool `mapstructure:"template_scalable" required:"false"`
|
|
|
|
//
|
|
|
|
TemplateTag string `mapstructure:"template_tag"`
|
2016-01-11 06:22:41 -05:00
|
|
|
|
2019-04-18 10:44:33 -04:00
|
|
|
Tags map[string]string `mapstructure:"tags"`
|
|
|
|
|
2017-07-17 03:05:12 -04:00
|
|
|
ctx interpolate.Context
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewConfig parses and validates the given config.
|
2019-12-17 05:25:56 -05:00
|
|
|
func (c *Config) Prepare(raws ...interface{}) error {
|
2016-01-11 06:22:41 -05:00
|
|
|
err := config.Decode(c, &config.DecodeOpts{
|
|
|
|
Interpolate: true,
|
|
|
|
InterpolateContext: &c.ctx,
|
2017-07-06 05:59:02 -04:00
|
|
|
InterpolateFilter: &interpolate.RenderFilter{
|
|
|
|
Exclude: []string{
|
|
|
|
"user_data",
|
|
|
|
},
|
|
|
|
},
|
2016-01-11 06:22:41 -05:00
|
|
|
}, raws...)
|
|
|
|
if err != nil {
|
2019-12-17 05:25:56 -05:00
|
|
|
return err
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
2020-11-19 15:07:02 -05:00
|
|
|
var errs *packersdk.MultiError
|
2016-01-11 06:22:41 -05:00
|
|
|
|
|
|
|
// Set some defaults.
|
2017-07-14 01:32:41 -04:00
|
|
|
if c.APIURL == "" {
|
|
|
|
// Default to environment variable for api_url, if it exists
|
|
|
|
c.APIURL = os.Getenv("CLOUDSTACK_API_URL")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.APIKey == "" {
|
|
|
|
// Default to environment variable for api_key, if it exists
|
|
|
|
c.APIKey = os.Getenv("CLOUDSTACK_API_KEY")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.SecretKey == "" {
|
|
|
|
// Default to environment variable for secret_key, if it exists
|
|
|
|
c.SecretKey = os.Getenv("CLOUDSTACK_SECRET_KEY")
|
|
|
|
}
|
|
|
|
|
2016-01-11 06:22:41 -05:00
|
|
|
if c.AsyncTimeout == 0 {
|
|
|
|
c.AsyncTimeout = 30 * time.Minute
|
|
|
|
}
|
|
|
|
|
2017-07-18 03:08:11 -04:00
|
|
|
if len(c.CIDRList) == 0 {
|
2017-07-14 01:50:54 -04:00
|
|
|
c.CIDRList = []string{"0.0.0.0/0"}
|
|
|
|
}
|
|
|
|
|
2016-01-11 06:22:41 -05:00
|
|
|
if c.InstanceName == "" {
|
|
|
|
c.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
|
|
|
}
|
|
|
|
|
2019-10-27 00:13:45 -04:00
|
|
|
if c.InstanceDisplayName == "" {
|
|
|
|
c.InstanceDisplayName = "Created by Packer"
|
|
|
|
}
|
|
|
|
|
2016-01-11 06:22:41 -05:00
|
|
|
if c.TemplateName == "" {
|
|
|
|
name, err := interpolate.Render("packer-{{timestamp}}", nil)
|
|
|
|
if err != nil {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs,
|
2016-01-11 06:22:41 -05:00
|
|
|
fmt.Errorf("Unable to parse template name: %s ", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
c.TemplateName = name
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.TemplateDisplayText == "" {
|
|
|
|
c.TemplateDisplayText = c.TemplateName
|
|
|
|
}
|
|
|
|
|
2017-07-26 15:34:11 -04:00
|
|
|
// If we are not given an explicit keypair, ssh_password or ssh_private_key_file,
|
|
|
|
// then create a temporary one, but only if the temporary_keypair_name has not
|
|
|
|
// been provided.
|
2018-08-29 05:23:59 -04:00
|
|
|
if c.Comm.SSHKeyPairName == "" && c.Comm.SSHTemporaryKeyPairName == "" &&
|
2018-08-23 10:35:07 -04:00
|
|
|
c.Comm.SSHPrivateKeyFile == "" && c.Comm.SSHPassword == "" {
|
2018-08-28 11:47:19 -04:00
|
|
|
c.Comm.SSHTemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
|
2017-07-16 02:26:48 -04:00
|
|
|
}
|
|
|
|
|
2016-01-11 06:22:41 -05:00
|
|
|
// Process required parameters.
|
|
|
|
if c.APIURL == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a api_url must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.APIKey == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a api_key must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.SecretKey == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a secret_key must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.Network == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a network must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
2017-07-18 03:08:11 -04:00
|
|
|
if c.CreateSecurityGroup && !c.Expunge {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("auto creating a temporary security group requires expunge"))
|
2017-07-18 03:08:11 -04:00
|
|
|
}
|
|
|
|
|
2016-01-11 06:22:41 -05:00
|
|
|
if c.ServiceOffering == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a service_offering must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.SourceISO == "" && c.SourceTemplate == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(
|
2016-01-11 06:22:41 -05:00
|
|
|
errs, errors.New("either source_iso or source_template must be specified"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.SourceISO != "" && c.SourceTemplate != "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(
|
2016-01-11 06:22:41 -05:00
|
|
|
errs, errors.New("only one of source_iso or source_template can be specified"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.SourceISO != "" && c.DiskOffering == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(
|
2016-01-11 06:22:41 -05:00
|
|
|
errs, errors.New("a disk_offering must be specified when using source_iso"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.SourceISO != "" && c.Hypervisor == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(
|
2016-01-11 06:22:41 -05:00
|
|
|
errs, errors.New("a hypervisor must be specified when using source_iso"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.TemplateOS == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a template_os must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.UserData != "" && c.UserDataFile != "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(
|
2016-01-11 06:22:41 -05:00
|
|
|
errs, errors.New("only one of user_data or user_data_file can be specified"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.UserDataFile != "" {
|
|
|
|
if _, err := os.Stat(c.UserDataFile); err != nil {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(
|
2016-01-11 06:22:41 -05:00
|
|
|
errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Zone == "" {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, errors.New("a zone must be specified"))
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
2020-11-19 15:07:02 -05:00
|
|
|
errs = packersdk.MultiErrorAppend(errs, es...)
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check for errors and return if we have any.
|
|
|
|
if errs != nil && len(errs.Errors) > 0 {
|
2019-12-17 05:25:56 -05:00
|
|
|
return errs
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|
|
|
|
|
2019-12-17 05:25:56 -05:00
|
|
|
return nil
|
2016-01-11 06:22:41 -05:00
|
|
|
}
|