roark 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +75 -0
- data/Rakefile +8 -0
- data/bin/roark +6 -0
- data/examples/example.json +52 -0
- data/lib/roark/ami.rb +148 -0
- data/lib/roark/ami_create_workflow.rb +50 -0
- data/lib/roark/aws/cloud_formation/create_stack.rb +35 -0
- data/lib/roark/aws/cloud_formation/destroy_stack.rb +17 -0
- data/lib/roark/aws/cloud_formation/stack_outputs.rb +17 -0
- data/lib/roark/aws/cloud_formation/stack_status.rb +21 -0
- data/lib/roark/aws/cloud_formation.rb +4 -0
- data/lib/roark/aws/connection.rb +27 -0
- data/lib/roark/aws/ec2/ami_state.rb +19 -0
- data/lib/roark/aws/ec2/create_ami.rb +20 -0
- data/lib/roark/aws/ec2/destroy_ami.rb +34 -0
- data/lib/roark/aws/ec2/instance_status.rb +15 -0
- data/lib/roark/aws/ec2/stop_instance.rb +15 -0
- data/lib/roark/aws/ec2.rb +5 -0
- data/lib/roark/aws.rb +5 -0
- data/lib/roark/cli/create.rb +76 -0
- data/lib/roark/cli/destroy.rb +53 -0
- data/lib/roark/cli/shared.rb +28 -0
- data/lib/roark/cli.rb +66 -0
- data/lib/roark/instance.rb +56 -0
- data/lib/roark/response.rb +15 -0
- data/lib/roark/stack.rb +63 -0
- data/lib/roark/version.rb +3 -0
- data/lib/roark.rb +17 -0
- data/roark.gemspec +26 -0
- data/spec/ami_create_workflow_spec.rb +34 -0
- data/spec/ami_spec.rb +217 -0
- data/spec/aws/cloud_formation/create_stack_spec.rb +20 -0
- data/spec/aws/connection_spec.rb +26 -0
- data/spec/aws/ec2/ami_state_spec.rb +11 -0
- data/spec/aws/ec2/create_ami_spec.rb +17 -0
- data/spec/aws/ec2/destroy_ami_spec.rb +32 -0
- data/spec/create_stack_spec.rb +20 -0
- data/spec/instance_spec.rb +80 -0
- data/spec/response_spec.rb +21 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/stack_spec.rb +99 -0
- metadata +155 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Intuit
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
[![Build Status](https://secure.travis-ci.org/intuit/roark.png)](http://travis-ci.org/intuit/roark)
|
2
|
+
|
3
|
+
# Roark
|
4
|
+
|
5
|
+
Howard Roark, master architect and builder of AMIs.
|
6
|
+
|
7
|
+
## Overview
|
8
|
+
|
9
|
+
* Roark builds AMIs from an Instance created by a Cloud Formation Stack.
|
10
|
+
* Roark expects to be provided with a Cloud Formation Template that can be used to create this stack.
|
11
|
+
* This template should create an Instance that is fully configured at bootstrap (via userdata, CloudInit, etc).
|
12
|
+
* The stack must provide the ID of the Instance to be converted to an AMI (IE. i-1234abcd) as a Cloud Formation Output named **InstanceId**.
|
13
|
+
* Once the stack is created, Roark will read the Instance ID from the Cloud Formation Output, stop the Instance, convert it into an AMI and destroy the stack.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
gem 'roark'
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install roark
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### CLI
|
32
|
+
|
33
|
+
Once you have a working templated, set your AWS Access and Secret Keys for the target account:
|
34
|
+
|
35
|
+
export AWS_ACCESS_KEY_ID=xxx
|
36
|
+
export AWS_SECRET_ACCESS_KEY=yyy
|
37
|
+
|
38
|
+
Create AMI
|
39
|
+
|
40
|
+
roark create -n NAME_OF_AMI
|
41
|
+
-r AWS_REGION \
|
42
|
+
-t PATH_TO_CLOUD_FORMATION_TEMPLATE \
|
43
|
+
-p 'Parameter1=value1' \
|
44
|
+
-p 'Parameter2=value2'
|
45
|
+
|
46
|
+
Destroy AMI
|
47
|
+
|
48
|
+
roark destroy -i AMI_ID -r AWS_REGION
|
49
|
+
|
50
|
+
## Example
|
51
|
+
|
52
|
+
The Cloud Formation Template [example.json](https://raw.github.com/intuit/roark/master/examples/example.json) in the /examples directory will create an AMI based off of the public Amazon Linux AMI.
|
53
|
+
|
54
|
+
It will write the file hello\_world.txt as part of bootstraping via userdata.
|
55
|
+
|
56
|
+
To create an AMI using this template, set your AWS Access Key and Secret Key:
|
57
|
+
|
58
|
+
export AWS_ACCESS_KEY_ID=xxx
|
59
|
+
export AWS_SECRET_ACCESS_KEY=yyy
|
60
|
+
|
61
|
+
Download the template:
|
62
|
+
|
63
|
+
wget https://raw.github.com/intuit/roark/master/examples/example.json
|
64
|
+
|
65
|
+
Create an AMI:
|
66
|
+
|
67
|
+
roark create -n roark-example-ami -t example.json
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
|
71
|
+
1. Fork it
|
72
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
73
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
74
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
75
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/roark
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
{
|
2
|
+
"AWSTemplateFormatVersion" : "2010-09-09",
|
3
|
+
|
4
|
+
"Description" : "AWS CloudFormation Sample Template to create instance and output ID for transforming to an image by Roark.",
|
5
|
+
|
6
|
+
"Mappings" : {
|
7
|
+
"AWSRegionArch2AMI" : {
|
8
|
+
"us-east-1" : { "PV64" : "ami-1624987f" },
|
9
|
+
"us-west-2" : { "PV64" : "ami-2a31bf1a" },
|
10
|
+
"us-west-1" : { "PV64" : "ami-1bf9de5e" },
|
11
|
+
"eu-west-1" : { "PV64" : "ami-c37474b7" },
|
12
|
+
"ap-southeast-1" : { "PV64" : "ami-a6a7e7f4" },
|
13
|
+
"ap-southeast-2" : { "PV64" : "ami-bd990e87" },
|
14
|
+
"ap-northeast-1" : { "PV64" : "ami-4e6cd34f" },
|
15
|
+
"sa-east-1" : { "PV64" : "ami-1e08d103" }
|
16
|
+
}
|
17
|
+
},
|
18
|
+
|
19
|
+
"Resources" : {
|
20
|
+
"Instance" : {
|
21
|
+
"Type" : "AWS::EC2::Instance",
|
22
|
+
"Properties" : {
|
23
|
+
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" }, "PV64" ]},
|
24
|
+
"InstanceType" : "m1.large",
|
25
|
+
"SecurityGroups" : [{ "Ref" : "Ec2SecurityGroup" }],
|
26
|
+
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
|
27
|
+
"#!/bin/bash\n",
|
28
|
+
"echo 'Hello World!' > /hello_world.txt", "\n"
|
29
|
+
]]}}
|
30
|
+
}
|
31
|
+
},
|
32
|
+
|
33
|
+
"Ec2SecurityGroup" : {
|
34
|
+
"Type" : "AWS::EC2::SecurityGroup",
|
35
|
+
"Properties" : {
|
36
|
+
"GroupDescription" : "HTTP and SSH access",
|
37
|
+
"SecurityGroupIngress" : [ {
|
38
|
+
"IpProtocol" : "tcp",
|
39
|
+
"FromPort" : "22", "ToPort" : "22",
|
40
|
+
"CidrIp" : "0.0.0.0/0"
|
41
|
+
} ]
|
42
|
+
}
|
43
|
+
}
|
44
|
+
},
|
45
|
+
|
46
|
+
"Outputs" : {
|
47
|
+
"InstanceId" : {
|
48
|
+
"Value" : { "Ref" : "Instance" },
|
49
|
+
"Description" : "ID of Instance"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
data/lib/roark/ami.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
module Roark
|
2
|
+
class Ami
|
3
|
+
|
4
|
+
attr_accessor :aws, :ami_id, :name
|
5
|
+
|
6
|
+
def initialize(args)
|
7
|
+
@aws = args[:aws]
|
8
|
+
@ami_id = args[:ami_id]
|
9
|
+
@name = args[:name]
|
10
|
+
|
11
|
+
@region = @aws.region
|
12
|
+
@logger = Roark.logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_instance(args)
|
16
|
+
@logger.info "Creating instance in '#{@region}'."
|
17
|
+
begin
|
18
|
+
instance.create :parameters => args[:parameters],
|
19
|
+
:template => args[:template]
|
20
|
+
rescue AWS::Errors::Base => e
|
21
|
+
return Response.new :code => 1, :message => e.message
|
22
|
+
end
|
23
|
+
|
24
|
+
@logger.info "Instance created."
|
25
|
+
Response.new :code => 0, :message => 'Instance created.'
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_ami
|
29
|
+
@logger.info "Creating AMI '#{@name}' from Instance '#{instance_id}'."
|
30
|
+
begin
|
31
|
+
ami = instance.create_ami_from_instance
|
32
|
+
@ami_id = ami.id
|
33
|
+
rescue AWS::Errors::Base => e
|
34
|
+
return Response.new :code => 1, :message => e.message
|
35
|
+
end
|
36
|
+
@logger.info "AMI '#{@ami_id}' created."
|
37
|
+
Response.new :code => 0, :message => "AMI '#{@ami_id}' created."
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop_instance
|
41
|
+
@logger.info "Stopping instance."
|
42
|
+
begin
|
43
|
+
instance.stop
|
44
|
+
rescue AWS::Errors::Base => e
|
45
|
+
return Response.new :code => 1, :message => e.message
|
46
|
+
end
|
47
|
+
Response.new :code => 0, :message => "Instance stopped."
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy_instance
|
51
|
+
begin
|
52
|
+
instance.destroy
|
53
|
+
rescue AWS::Errors::Base => e
|
54
|
+
return Response.new :code => 1, :message => e.message
|
55
|
+
end
|
56
|
+
@logger.info "Instance destroyed."
|
57
|
+
Response.new :code => 0, :message => "Instance destroyed."
|
58
|
+
end
|
59
|
+
|
60
|
+
def destroy
|
61
|
+
@logger.info "Destroying AMI '#{@ami_id}'."
|
62
|
+
begin
|
63
|
+
ec2_destroy_ami.destroy @ami_id
|
64
|
+
rescue AWS::Errors::Base, AWS::Core::Resource::NotFound => e
|
65
|
+
return Response.new :code => 1, :message => e.message
|
66
|
+
end
|
67
|
+
@logger.info "Destroy completed succesfully."
|
68
|
+
Response.new :code => 0, :message => "Destroy completed succesfully."
|
69
|
+
end
|
70
|
+
|
71
|
+
def wait_for_instance
|
72
|
+
while instance.in_progress? || !instance.exists?
|
73
|
+
@logger.info "Waiting for instance to come online."
|
74
|
+
sleep 60
|
75
|
+
end
|
76
|
+
|
77
|
+
if instance.success?
|
78
|
+
msg = "Instance '#{instance_id}' completed succesfully."
|
79
|
+
@logger.info msg
|
80
|
+
Response.new :code => 0, :message => msg
|
81
|
+
else
|
82
|
+
msg = "Instance did not complete succesfully."
|
83
|
+
@logger.info msg
|
84
|
+
Response.new :code => 1, :message => msg
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def wait_for_instance_to_stop
|
89
|
+
while instance.status != :stopped
|
90
|
+
@logger.info "Waiting for instance '#{instance_id}' to stop. Current state: '#{instance.status}'."
|
91
|
+
sleep 15
|
92
|
+
end
|
93
|
+
Response.new :code => 0, :message => "Instance stopped."
|
94
|
+
end
|
95
|
+
|
96
|
+
def wait_for_ami
|
97
|
+
while !exists? || pending?
|
98
|
+
@logger.info "Waiting for AMI creation to complete. Current state: '#{state}'."
|
99
|
+
sleep 15
|
100
|
+
end
|
101
|
+
|
102
|
+
if available?
|
103
|
+
msg = "AMI completed succesfully."
|
104
|
+
@logger.info msg
|
105
|
+
Response.new :code => 0, :message => msg
|
106
|
+
else
|
107
|
+
msg = "AMI did not complete succesfully."
|
108
|
+
@logger.info msg
|
109
|
+
Response.new :code => 1, :message => msg
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def available?
|
114
|
+
state == :available
|
115
|
+
end
|
116
|
+
|
117
|
+
def pending?
|
118
|
+
state == :pending
|
119
|
+
end
|
120
|
+
|
121
|
+
def exists?
|
122
|
+
ec2_ami_state.exists? @ami_id
|
123
|
+
end
|
124
|
+
|
125
|
+
def state
|
126
|
+
ec2_ami_state.state @ami_id
|
127
|
+
end
|
128
|
+
|
129
|
+
def instance_id
|
130
|
+
instance.instance_id
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def instance
|
136
|
+
@instance ||= Instance.new :aws => @aws, :name => @name
|
137
|
+
end
|
138
|
+
|
139
|
+
def ec2_ami_state
|
140
|
+
@ec2_ami_state ||= Roark::Aws::Ec2::AmiState.new @aws
|
141
|
+
end
|
142
|
+
|
143
|
+
def ec2_destroy_ami
|
144
|
+
@ec2_destroy_ami ||= Roark::Aws::Ec2::DestroyAmi.new @aws
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Roark
|
2
|
+
class AmiCreateWorkflow
|
3
|
+
|
4
|
+
def initialize(args)
|
5
|
+
@ami = args[:ami]
|
6
|
+
@parameters = args[:parameters]
|
7
|
+
@template = args[:template]
|
8
|
+
@logger = Roark.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
%w(create_instance wait_for_instance stop_instance wait_for_instance_to_stop
|
13
|
+
create_ami wait_for_ami destroy_instance).each do |m|
|
14
|
+
response = self.send m.to_sym
|
15
|
+
return response unless response.success?
|
16
|
+
end
|
17
|
+
Response.new :code => 0, :message => "AMI create workflow completed succesfully."
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_instance
|
21
|
+
@ami.create_instance :parameters => @parameters,
|
22
|
+
:template => @template
|
23
|
+
end
|
24
|
+
|
25
|
+
def wait_for_instance
|
26
|
+
@ami.wait_for_instance
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop_instance
|
30
|
+
@ami.stop_instance
|
31
|
+
end
|
32
|
+
|
33
|
+
def wait_for_instance_to_stop
|
34
|
+
@ami.wait_for_instance_to_stop
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_ami
|
38
|
+
@ami.create_ami
|
39
|
+
end
|
40
|
+
|
41
|
+
def wait_for_ami
|
42
|
+
@ami.wait_for_ami
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy_instance
|
46
|
+
@ami.destroy_instance
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Roark
|
2
|
+
module Aws
|
3
|
+
module CloudFormation
|
4
|
+
class CreateStack
|
5
|
+
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(args)
|
11
|
+
name = args[:name]
|
12
|
+
parameters = args[:parameters]
|
13
|
+
template = args[:template]
|
14
|
+
|
15
|
+
@connection.cf.stacks.create name, template, { :capabilities => capabilities,
|
16
|
+
:parameters => format_parameters(parameters) }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def format_parameters(parameters={})
|
22
|
+
parameters.map do |p|
|
23
|
+
{ :parameter_key => p.first,
|
24
|
+
:parameter_value => p.last }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def capabilities
|
29
|
+
['CAPABILITY_IAM']
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Roark
|
2
|
+
module Aws
|
3
|
+
module CloudFormation
|
4
|
+
class StackStatus
|
5
|
+
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def status(name)
|
11
|
+
@connection.cf.stacks[name].status
|
12
|
+
end
|
13
|
+
|
14
|
+
def exists?(name)
|
15
|
+
@connection.cf.stacks[name].exists?
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Roark
|
2
|
+
module Aws
|
3
|
+
class Connection
|
4
|
+
|
5
|
+
attr_accessor :region
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@access_key_id = args[:access_key_id]
|
9
|
+
@secret_access_key = args[:secret_access_key]
|
10
|
+
@region = args[:region]
|
11
|
+
end
|
12
|
+
|
13
|
+
def cf
|
14
|
+
@cf ||= AWS::CloudFormation.new :access_key_id => @access_key_id,
|
15
|
+
:secret_access_key => @secret_access_key,
|
16
|
+
:region => @region
|
17
|
+
end
|
18
|
+
|
19
|
+
def ec2
|
20
|
+
@ec2 ||= AWS::EC2.new :access_key_id => @access_key_id,
|
21
|
+
:secret_access_key => @secret_access_key,
|
22
|
+
:region => @region
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Roark
|
2
|
+
module Aws
|
3
|
+
module Ec2
|
4
|
+
class AmiState
|
5
|
+
def initialize(connection)
|
6
|
+
@connection = connection
|
7
|
+
end
|
8
|
+
|
9
|
+
def state(ami_id)
|
10
|
+
@connection.ec2.images[ami_id].state
|
11
|
+
end
|
12
|
+
|
13
|
+
def exists?(ami_id)
|
14
|
+
@connection.ec2.images[ami_id].exists?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Roark
|
2
|
+
module Aws
|
3
|
+
module Ec2
|
4
|
+
class CreateAmi
|
5
|
+
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(args)
|
11
|
+
instance_id = args[:instance_id]
|
12
|
+
name = args[:name]
|
13
|
+
@connection.ec2.images.create :instance_id => instance_id,
|
14
|
+
:name => name
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Roark
|
2
|
+
module Aws
|
3
|
+
module Ec2
|
4
|
+
class DestroyAmi
|
5
|
+
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
@logger = Roark.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def destroy(ami_id)
|
12
|
+
ami = @connection.ec2.images[ami_id]
|
13
|
+
|
14
|
+
@block_device_mappings = ami.block_device_mappings
|
15
|
+
|
16
|
+
@logger.info "Deleting AMI '#{ami_id}'."
|
17
|
+
ami.delete
|
18
|
+
delete_snapshots
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def delete_snapshots
|
24
|
+
@block_device_mappings.each_value do |v|
|
25
|
+
snapshot_id = v[:snapshot_id]
|
26
|
+
@logger.info "Deleting snapshot '#{snapshot_id}'."
|
27
|
+
@connection.ec2.snapshots[snapshot_id].delete
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|