2020-08-17 11:09:19 -04:00
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type AWSPollingConfig
2013-07-20 22:58:27 -04:00
package common
2013-05-21 03:55:32 -04:00
import (
2018-12-06 16:30:20 -05:00
"fmt"
2013-05-21 03:55:32 -04:00
"log"
2014-09-29 21:14:52 -04:00
"os"
"strconv"
2019-11-08 05:15:24 -05:00
"strings"
2016-02-12 02:53:40 -05:00
"time"
2015-04-05 17:58:48 -04:00
2018-05-31 14:29:04 -04:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
2015-06-03 17:13:52 -04:00
"github.com/aws/aws-sdk-go/service/ec2"
2019-05-03 17:47:09 -04:00
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
2018-01-19 19:18:44 -05:00
"github.com/hashicorp/packer/helper/multistep"
2013-05-21 03:55:32 -04:00
)
2013-08-03 19:21:01 -04:00
// StateRefreshFunc is a function type used for StateChangeConf that is
// responsible for refreshing the item being watched for a state change.
//
// It returns three results. `result` is any object that will be returned
// as the final object after waiting for state change. This allows you to
// return the final updated object, for example an EC2 instance after refreshing
// it.
//
// `state` is the latest state of that object. And `err` is any error that
// may have happened while refreshing the state.
type StateRefreshFunc func ( ) ( result interface { } , state string , err error )
// StateChangeConf is the configuration struct used for `WaitForState`.
2013-07-25 21:49:15 -04:00
type StateChangeConf struct {
Pending [ ] string
2013-08-03 19:21:01 -04:00
Refresh StateRefreshFunc
2013-08-31 15:58:55 -04:00
StepState multistep . StateBag
2013-07-25 21:49:15 -04:00
Target string
}
2018-05-31 14:29:04 -04:00
// Following are wrapper functions that use Packer's environment-variables to
2019-07-25 00:49:51 -04:00
// determine retry logic, then call the AWS SDK's built-in waiters.
2013-09-12 23:33:32 -04:00
2020-08-17 11:09:19 -04:00
// Polling configuration for the AWS waiter. Configures the waiter for resources creation or actions like attaching
// volumes or importing image.
// Usage example:
//
// In JSON:
// ```json
// "aws_polling" : {
// "delay_seconds": 30,
// "max_attempts": 50
// }
// ```
//
// In HCL2:
// ```hcl
// aws_polling {
// delay_seconds = 30
// max_attempts = 50
// }
// ```
//
type AWSPollingConfig struct {
// Specifies the maximum number of attempts the waiter will check for resource state.
// This value can also be set via the AWS_MAX_ATTEMPTS.
2020-08-17 11:58:22 -04:00
// If both option and environment variable are set, the max_attempts will be considered over the AWS_MAX_ATTEMPTS.
// If none is set, defaults to AWS waiter default which is 40 max_attempts.
2020-08-17 11:09:19 -04:00
MaxAttempts int ` mapstructure:"max_attempts" required:"false" `
// Specifies the delay in seconds between attempts to check the resource state.
// This value can also be set via the AWS_POLL_DELAY_SECONDS.
// If both option and environment variable are set, the delay_seconds will be considered over the AWS_POLL_DELAY_SECONDS.
// If none is set, defaults to AWS waiter default which is 15 seconds.
DelaySeconds int ` mapstructure:"delay_seconds" required:"false" `
}
func ( w * AWSPollingConfig ) WaitUntilAMIAvailable ( ctx aws . Context , conn ec2iface . EC2API , imageId string ) error {
2018-05-31 14:29:04 -04:00
imageInput := ec2 . DescribeImagesInput {
ImageIds : [ ] * string { & imageId } ,
2013-09-12 23:33:32 -04:00
}
2020-08-17 11:09:19 -04:00
waitOpts := w . getWaiterOptions ( )
2018-08-17 16:27:19 -04:00
if len ( waitOpts ) == 0 {
2018-08-17 16:30:02 -04:00
// Bump this default to 30 minutes because the aws default
2018-08-17 16:27:19 -04:00
// of ten minutes doesn't work for some of our long-running copies.
2018-08-17 16:30:02 -04:00
waitOpts = append ( waitOpts , request . WithWaiterMaxAttempts ( 120 ) )
2018-08-17 16:27:19 -04:00
}
2018-06-01 19:17:30 -04:00
err := conn . WaitUntilImageAvailableWithContext (
ctx ,
2018-05-31 14:29:04 -04:00
& imageInput ,
2018-08-17 16:27:19 -04:00
waitOpts ... )
2019-11-08 05:15:24 -05:00
if err != nil {
if strings . Contains ( err . Error ( ) , request . WaiterResourceNotReadyErrorCode ) {
err = fmt . Errorf ( "Failed with ResourceNotReady error, which can " +
"have a variety of causes. For help troubleshooting, check " +
"our docs: " +
"https://www.packer.io/docs/builders/amazon.html#resourcenotready-error\n" +
"original error: %s" , err . Error ( ) )
}
}
2018-05-31 14:29:04 -04:00
return err
}
2013-07-29 21:47:43 -04:00
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilInstanceRunning ( ctx aws . Context , conn * ec2 . EC2 , instanceId string ) error {
2020-02-05 18:09:09 -05:00
instanceInput := ec2 . DescribeInstancesInput {
InstanceIds : [ ] * string { & instanceId } ,
}
err := conn . WaitUntilInstanceRunningWithContext (
ctx ,
& instanceInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2020-02-05 18:09:09 -05:00
return err
}
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilInstanceTerminated ( ctx aws . Context , conn * ec2 . EC2 , instanceId string ) error {
2018-05-31 14:29:04 -04:00
instanceInput := ec2 . DescribeInstancesInput {
InstanceIds : [ ] * string { & instanceId } ,
2013-07-29 21:47:43 -04:00
}
2018-05-31 14:29:04 -04:00
2018-06-01 19:17:30 -04:00
err := conn . WaitUntilInstanceTerminatedWithContext (
ctx ,
2018-05-31 14:29:04 -04:00
& instanceInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
2013-07-29 21:47:43 -04:00
}
2018-05-31 14:29:04 -04:00
// This function works for both requesting and cancelling spot instances.
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilSpotRequestFulfilled ( ctx aws . Context , conn * ec2 . EC2 , spotRequestId string ) error {
2018-05-31 14:29:04 -04:00
spotRequestInput := ec2 . DescribeSpotInstanceRequestsInput {
SpotInstanceRequestIds : [ ] * string { & spotRequestId } ,
}
2014-05-07 13:13:27 -04:00
2018-06-01 19:17:30 -04:00
err := conn . WaitUntilSpotInstanceRequestFulfilledWithContext (
ctx ,
2018-05-31 14:29:04 -04:00
& spotRequestInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
}
2014-05-07 13:13:27 -04:00
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilVolumeAvailable ( ctx aws . Context , conn * ec2 . EC2 , volumeId string ) error {
2018-05-31 14:29:04 -04:00
volumeInput := ec2 . DescribeVolumesInput {
VolumeIds : [ ] * string { & volumeId } ,
2014-05-07 13:13:27 -04:00
}
2018-05-31 14:29:04 -04:00
2018-06-01 19:17:30 -04:00
err := conn . WaitUntilVolumeAvailableWithContext (
ctx ,
2018-05-31 14:29:04 -04:00
& volumeInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
2014-05-07 13:13:27 -04:00
}
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilSnapshotDone ( ctx aws . Context , conn * ec2 . EC2 , snapshotID string ) error {
2018-05-31 14:29:04 -04:00
snapInput := ec2 . DescribeSnapshotsInput {
SnapshotIds : [ ] * string { & snapshotID } ,
}
2018-06-01 19:17:30 -04:00
err := conn . WaitUntilSnapshotCompletedWithContext (
ctx ,
2018-05-31 14:29:04 -04:00
& snapInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
}
2016-02-12 02:53:40 -05:00
2018-05-31 14:29:04 -04:00
// Wrappers for our custom AWS waiters
2016-02-12 02:53:40 -05:00
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilVolumeAttached ( ctx aws . Context , conn * ec2 . EC2 , volumeId string ) error {
2018-05-31 14:29:04 -04:00
volumeInput := ec2 . DescribeVolumesInput {
VolumeIds : [ ] * string { & volumeId } ,
2016-02-12 02:53:40 -05:00
}
2018-05-31 14:29:04 -04:00
err := WaitForVolumeToBeAttached ( conn ,
2018-06-01 19:17:30 -04:00
ctx ,
2018-05-31 14:29:04 -04:00
& volumeInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
2015-11-22 21:55:09 -05:00
}
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilVolumeDetached ( ctx aws . Context , conn * ec2 . EC2 , volumeId string ) error {
2018-05-31 14:29:04 -04:00
volumeInput := ec2 . DescribeVolumesInput {
VolumeIds : [ ] * string { & volumeId } ,
}
2013-09-12 23:37:14 -04:00
2018-06-01 19:17:30 -04:00
err := WaitForVolumeToBeDetached ( conn ,
ctx ,
2018-05-31 14:29:04 -04:00
& volumeInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
}
2013-07-29 21:47:43 -04:00
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) WaitUntilImageImported ( ctx aws . Context , conn * ec2 . EC2 , taskID string ) error {
2018-05-31 14:29:04 -04:00
importInput := ec2 . DescribeImportImageTasksInput {
ImportTaskIds : [ ] * string { & taskID } ,
}
2013-05-21 03:55:32 -04:00
2018-05-31 14:29:04 -04:00
err := WaitForImageToBeImported ( conn ,
2018-06-01 19:17:30 -04:00
ctx ,
2018-05-31 14:29:04 -04:00
& importInput ,
2020-08-17 11:09:19 -04:00
w . getWaiterOptions ( ) ... )
2018-05-31 14:29:04 -04:00
return err
}
2013-06-04 14:29:59 -04:00
2018-05-31 14:29:04 -04:00
// Custom waiters using AWS's request.Waiter
func WaitForVolumeToBeAttached ( c * ec2 . EC2 , ctx aws . Context , input * ec2 . DescribeVolumesInput , opts ... request . WaiterOption ) error {
w := request . Waiter {
Name : "DescribeVolumes" ,
MaxAttempts : 40 ,
Delay : request . ConstantWaiterDelay ( 5 * time . Second ) ,
Acceptors : [ ] request . WaiterAcceptor {
{
State : request . SuccessWaiterState ,
Matcher : request . PathAllWaiterMatch ,
2018-06-04 18:34:20 -04:00
Argument : "Volumes[].Attachments[].State" ,
2018-05-31 14:29:04 -04:00
Expected : "attached" ,
} ,
} ,
Logger : c . Config . Logger ,
NewRequest : func ( opts [ ] request . Option ) ( * request . Request , error ) {
var inCpy * ec2 . DescribeVolumesInput
if input != nil {
tmp := * input
inCpy = & tmp
2013-09-05 20:19:23 -04:00
}
2018-05-31 14:29:04 -04:00
req , _ := c . DescribeVolumesRequest ( inCpy )
req . SetContext ( ctx )
req . ApplyOptions ( opts ... )
return req , nil
} ,
}
2018-08-31 16:27:44 -04:00
w . ApplyOptions ( opts ... )
2018-05-31 14:29:04 -04:00
return w . WaitWithContext ( ctx )
}
2013-06-04 14:29:59 -04:00
2018-05-31 14:29:04 -04:00
func WaitForVolumeToBeDetached ( c * ec2 . EC2 , ctx aws . Context , input * ec2 . DescribeVolumesInput , opts ... request . WaiterOption ) error {
w := request . Waiter {
Name : "DescribeVolumes" ,
MaxAttempts : 40 ,
Delay : request . ConstantWaiterDelay ( 5 * time . Second ) ,
Acceptors : [ ] request . WaiterAcceptor {
{
State : request . SuccessWaiterState ,
Matcher : request . PathAllWaiterMatch ,
2018-06-04 18:34:20 -04:00
Argument : "length(Volumes[].Attachments[]) == `0`" ,
Expected : true ,
2018-05-31 14:29:04 -04:00
} ,
} ,
Logger : c . Config . Logger ,
NewRequest : func ( opts [ ] request . Option ) ( * request . Request , error ) {
var inCpy * ec2 . DescribeVolumesInput
if input != nil {
tmp := * input
inCpy = & tmp
}
req , _ := c . DescribeVolumesRequest ( inCpy )
req . SetContext ( ctx )
req . ApplyOptions ( opts ... )
return req , nil
} ,
2013-05-21 03:55:32 -04:00
}
2018-08-31 16:27:44 -04:00
w . ApplyOptions ( opts ... )
2018-05-31 14:29:04 -04:00
return w . WaitWithContext ( ctx )
2013-05-21 03:55:32 -04:00
}
2014-10-14 18:39:13 -04:00
2018-05-31 14:29:04 -04:00
func WaitForImageToBeImported ( c * ec2 . EC2 , ctx aws . Context , input * ec2 . DescribeImportImageTasksInput , opts ... request . WaiterOption ) error {
w := request . Waiter {
Name : "DescribeImages" ,
2018-10-09 16:08:42 -04:00
MaxAttempts : 720 ,
2018-05-31 14:29:04 -04:00
Delay : request . ConstantWaiterDelay ( 5 * time . Second ) ,
Acceptors : [ ] request . WaiterAcceptor {
{
State : request . SuccessWaiterState ,
Matcher : request . PathAllWaiterMatch ,
2018-06-04 18:34:20 -04:00
Argument : "ImportImageTasks[].Status" ,
2018-05-31 14:29:04 -04:00
Expected : "completed" ,
} ,
2018-09-20 14:44:37 -04:00
{
State : request . FailureWaiterState ,
Matcher : request . PathAnyWaiterMatch ,
Argument : "ImportImageTasks[].Status" ,
Expected : "deleted" ,
} ,
2018-05-31 14:29:04 -04:00
} ,
Logger : c . Config . Logger ,
NewRequest : func ( opts [ ] request . Option ) ( * request . Request , error ) {
var inCpy * ec2 . DescribeImportImageTasksInput
if input != nil {
tmp := * input
inCpy = & tmp
}
req , _ := c . DescribeImportImageTasksRequest ( inCpy )
req . SetContext ( ctx )
req . ApplyOptions ( opts ... )
return req , nil
} ,
2014-10-14 18:39:13 -04:00
}
2018-08-31 16:27:44 -04:00
w . ApplyOptions ( opts ... )
2018-05-31 14:29:04 -04:00
return w . WaitWithContext ( ctx )
}
2014-10-14 18:39:13 -04:00
2018-05-31 14:29:04 -04:00
// This helper function uses the environment variables AWS_TIMEOUT_SECONDS and
// AWS_POLL_DELAY_SECONDS to generate waiter options that can be passed into any
// request.Waiter function. These options will control how many times the waiter
// will retry the request, as well as how long to wait between the retries.
2018-06-04 18:34:20 -04:00
// DEFAULTING BEHAVIOR:
// if AWS_POLL_DELAY_SECONDS is set but the others are not, Packer will set this
// poll delay and use the waiter-specific default
// if AWS_TIMEOUT_SECONDS is set but AWS_MAX_ATTEMPTS is not, Packer will use
// AWS_TIMEOUT_SECONDS and _either_ AWS_POLL_DELAY_SECONDS _or_ 2 if the user has not set AWS_POLL_DELAY_SECONDS, to determine a max number of attempts to make.
// if AWS_TIMEOUT_SECONDS, _and_ AWS_MAX_ATTEMPTS are both set,
// AWS_TIMEOUT_SECONDS will be ignored.
// if AWS_MAX_ATTEMPTS is set but AWS_POLL_DELAY_SECONDS is not, then we will
// use waiter-specific defaults.
2018-06-29 19:33:26 -04:00
type envInfo struct {
envKey string
Val int
overridden bool
}
type overridableWaitVars struct {
awsPollDelaySeconds envInfo
awsMaxAttempts envInfo
awsTimeoutSeconds envInfo
}
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) getWaiterOptions ( ) [ ] request . WaiterOption {
2018-06-29 19:33:26 -04:00
envOverrides := getEnvOverrides ( )
2020-08-17 11:09:19 -04:00
if w . MaxAttempts != 0 {
envOverrides . awsMaxAttempts . Val = w . MaxAttempts
envOverrides . awsMaxAttempts . overridden = true
}
if w . DelaySeconds != 0 {
envOverrides . awsPollDelaySeconds . Val = w . DelaySeconds
envOverrides . awsPollDelaySeconds . overridden = true
}
2018-06-29 19:33:26 -04:00
waitOpts := applyEnvOverrides ( envOverrides )
return waitOpts
}
2018-07-11 16:10:38 -04:00
func getOverride ( varInfo envInfo ) envInfo {
override := os . Getenv ( varInfo . envKey )
if override != "" {
n , err := strconv . Atoi ( override )
if err != nil {
log . Printf ( "Invalid %s '%s', using default" , varInfo . envKey , override )
} else {
varInfo . overridden = true
varInfo . Val = n
}
}
return varInfo
}
2018-06-29 19:33:26 -04:00
func getEnvOverrides ( ) overridableWaitVars {
2018-08-31 16:27:44 -04:00
// Load env vars from environment.
2018-06-29 19:33:26 -04:00
envValues := overridableWaitVars {
2018-09-17 14:10:50 -04:00
envInfo { "AWS_POLL_DELAY_SECONDS" , 2 , false } ,
2018-06-29 19:33:26 -04:00
envInfo { "AWS_MAX_ATTEMPTS" , 0 , false } ,
2018-08-31 16:27:44 -04:00
envInfo { "AWS_TIMEOUT_SECONDS" , 0 , false } ,
2018-06-29 19:33:26 -04:00
}
2018-07-11 16:10:38 -04:00
envValues . awsMaxAttempts = getOverride ( envValues . awsMaxAttempts )
envValues . awsPollDelaySeconds = getOverride ( envValues . awsPollDelaySeconds )
envValues . awsTimeoutSeconds = getOverride ( envValues . awsTimeoutSeconds )
2018-06-29 19:33:26 -04:00
return envValues
}
2020-08-17 11:09:19 -04:00
func ( w * AWSPollingConfig ) LogEnvOverrideWarnings ( ) {
pollDelayEnv := os . Getenv ( "AWS_POLL_DELAY_SECONDS" )
timeoutSecondsEnv := os . Getenv ( "AWS_TIMEOUT_SECONDS" )
maxAttemptsEnv := os . Getenv ( "AWS_MAX_ATTEMPTS" )
maxAttemptsIsSet := maxAttemptsEnv != "" || w . MaxAttempts != 0
timeoutSecondsIsSet := timeoutSecondsEnv != ""
pollDelayIsSet := pollDelayEnv != "" || w . DelaySeconds != 0
2018-12-06 16:30:20 -05:00
2020-08-17 11:09:19 -04:00
if maxAttemptsIsSet && timeoutSecondsIsSet {
2018-12-06 16:30:20 -05:00
warning := fmt . Sprintf ( "[WARNING] (aws): AWS_MAX_ATTEMPTS and " +
"AWS_TIMEOUT_SECONDS are both set. Packer will use " +
"AWS_MAX_ATTEMPTS and discard AWS_TIMEOUT_SECONDS." )
2020-08-17 11:09:19 -04:00
if ! pollDelayIsSet {
2018-12-06 16:30:20 -05:00
warning = fmt . Sprintf ( "%s Since you have not set the poll delay, " +
"Packer will default to a 2-second delay." , warning )
}
log . Printf ( warning )
2020-08-17 11:09:19 -04:00
} else if timeoutSecondsIsSet {
2018-12-06 16:30:20 -05:00
log . Printf ( "[WARNING] (aws): env var AWS_TIMEOUT_SECONDS is " +
2020-08-17 11:09:19 -04:00
"deprecated in favor of AWS_MAX_ATTEMPTS env or aws_polling_max_attempts config option. " +
"If you have not explicitly set AWS_POLL_DELAY_SECONDS env or aws_polling_delay_seconds config option, " +
"we are defaulting to a poll delay of 2 seconds, regardless of the AWS waiter's default." )
2018-12-06 16:30:20 -05:00
}
2020-08-17 11:09:19 -04:00
if ! maxAttemptsIsSet && ! timeoutSecondsIsSet && ! pollDelayIsSet {
2018-12-06 16:30:20 -05:00
log . Printf ( "[INFO] (aws): No AWS timeout and polling overrides have been set. " +
"Packer will default to waiter-specific delays and timeouts. If you would " +
"like to customize the length of time between retries and max " +
"number of retries you may do so by setting the environment " +
2020-08-17 11:09:19 -04:00
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS or the " +
"configuration options aws_polling_delay_seconds and aws_polling_max_attempts " +
"to your desired values." )
2018-12-06 16:30:20 -05:00
}
}
2018-06-29 19:33:26 -04:00
func applyEnvOverrides ( envOverrides overridableWaitVars ) [ ] request . WaiterOption {
2018-06-04 18:34:20 -04:00
waitOpts := make ( [ ] request . WaiterOption , 0 )
// If user has set poll delay seconds, overwrite it. If user has NOT,
// default to a poll delay of 2 seconds
2018-06-29 19:33:26 -04:00
if envOverrides . awsPollDelaySeconds . overridden {
delaySeconds := request . ConstantWaiterDelay ( time . Duration ( envOverrides . awsPollDelaySeconds . Val ) * time . Second )
2018-06-04 18:34:20 -04:00
waitOpts = append ( waitOpts , request . WithWaiterDelay ( delaySeconds ) )
}
2014-09-29 21:14:52 -04:00
2018-06-04 18:34:20 -04:00
// If user has set max attempts, overwrite it. If user hasn't set max
// attempts, default to whatever the waiter has set as a default.
2018-06-29 19:33:26 -04:00
if envOverrides . awsMaxAttempts . overridden {
waitOpts = append ( waitOpts , request . WithWaiterMaxAttempts ( envOverrides . awsMaxAttempts . Val ) )
} else if envOverrides . awsTimeoutSeconds . overridden {
maxAttempts := envOverrides . awsTimeoutSeconds . Val / envOverrides . awsPollDelaySeconds . Val
2018-06-04 18:34:20 -04:00
// override the delay so we can get the timeout right
2018-06-29 19:33:26 -04:00
if ! envOverrides . awsPollDelaySeconds . overridden {
delaySeconds := request . ConstantWaiterDelay ( time . Duration ( envOverrides . awsPollDelaySeconds . Val ) * time . Second )
2018-06-04 18:34:20 -04:00
waitOpts = append ( waitOpts , request . WithWaiterDelay ( delaySeconds ) )
2014-09-29 21:14:52 -04:00
}
2018-06-04 18:34:20 -04:00
waitOpts = append ( waitOpts , request . WithWaiterMaxAttempts ( maxAttempts ) )
2014-09-29 21:14:52 -04:00
}
2018-06-04 18:34:20 -04:00
return waitOpts
2014-09-29 21:14:52 -04:00
}