opsicle 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -89,6 +89,15 @@ opsicle ssh-key staging <key-file>
89
89
  opsicle monitor staging
90
90
 
91
91
  ```
92
+ ### Updating Custom Chef Recipes
93
+ ```bash
94
+ # Upload a cookbooks directory to S3 and update the stack's custom cookbooks
95
+ opsicle chef-update staging --bucket-name my-opsworks-cookbooks
96
+
97
+ ```
98
+ This command accepts a --path argument to the directory of cookbooks to upload. It defaults to 'cookbooks'.
99
+ It also accepts a --bucket-name for the base s3 bucket. This flag is required.
100
+
92
101
 
93
102
  Opsicle accepts a `--verbose` flag or the VERBOSE environment variable to show additional information as commands are run.
94
103
 
@@ -114,4 +114,19 @@ command 'opsworks-url' do |c|
114
114
  end
115
115
  end
116
116
 
117
+ desc "Update the Stack Custom Chef Cookbooks"
118
+ arg_name '<environment>'
119
+ command 'chef-update' do |c|
120
+ c.flag [:path],
121
+ :desc => "Path to the directory of chef cookbooks to be uploaded to s3",
122
+ :default_value => "cookbooks"
123
+ c.flag [:"bucket-name"],
124
+ :desc => "The S3 bucket name to upload the cookbooks to"
125
+ c.action do |global_options, options, args|
126
+ raise ArgumentError, "Environment is required" unless args.first
127
+
128
+ Opsicle::ChefUpdate.new(*args).execute global_options.merge(options)
129
+ end
130
+ end
131
+
117
132
  exit run(ARGV)
@@ -2,21 +2,23 @@ require 'opsicle/config'
2
2
 
3
3
  module Opsicle
4
4
  class Client
5
- attr_reader :aws_client
5
+ attr_reader :opsworks
6
+ attr_reader :s3
6
7
  attr_reader :config
7
8
 
8
9
  def initialize(environment)
9
10
  @config = Config.new(environment)
10
11
  @config.configure_aws!
11
- @aws_client = AWS::OpsWorks.new.client
12
+ @opsworks = AWS::OpsWorks.new.client
13
+ @s3 = AWS::S3.new
12
14
  end
13
15
 
14
16
  def run_command(command, options={})
15
- aws_client.create_deployment(command_options(command, options))
17
+ opsworks.create_deployment(command_options(command, options))
16
18
  end
17
19
 
18
20
  def api_call(command, options={})
19
- aws_client.public_send(command, options)
21
+ opsworks.public_send(command, options)
20
22
  end
21
23
 
22
24
  def opsworks_url
@@ -1,6 +1,7 @@
1
1
  require 'opsicle/client'
2
2
 
3
3
  require "opsicle/commands/deploy"
4
+ require "opsicle/commands/chef_update"
4
5
  require "opsicle/commands/list"
5
6
  require "opsicle/commands/ssh"
6
7
  require "opsicle/commands/ssh_key"
@@ -0,0 +1,54 @@
1
+ require 'opsicle/s3_bucket'
2
+ require 'zlib'
3
+ require 'archive/tar/minitar'
4
+
5
+ module Opsicle
6
+ class ChefUpdate
7
+ attr_reader :client
8
+ attr_reader :tar_file
9
+ attr_reader :stack
10
+
11
+ def initialize(environment)
12
+ @environment = environment
13
+ @client = Client.new(environment)
14
+ @stack = Stack.new(@client)
15
+ @tar_file = "#{@stack.name}.tgz"
16
+ end
17
+
18
+ def execute(options={ monitor: true })
19
+ tar_cookbooks(options[:path])
20
+ s3_upload(options[:"bucket-name"])
21
+ cleanup_tar
22
+ update_custom_cookbooks
23
+ launch_stack_monitor(options)
24
+ end
25
+
26
+ private
27
+
28
+ def tar_cookbooks(cookbooks_dir)
29
+ tgz = Zlib::GzipWriter.new(File.open(tar_file, 'wb'))
30
+ package = Dir[cookbooks_dir].entries.reject{ |entry| entry =~ /^\.\.?$/ }
31
+ Archive::Tar::Minitar.pack(package, tgz)
32
+ end
33
+
34
+ def s3_upload(bucket_name)
35
+ bucket = S3Bucket.new(@client, bucket_name)
36
+ bucket.update(tar_file)
37
+ end
38
+
39
+ def cleanup_tar
40
+ FileUtils.rm(tar_file)
41
+ end
42
+
43
+ def update_custom_cookbooks
44
+ Output.say "Starting OpsWorks Custom Cookboks Update..."
45
+ client.run_command('update_custom_cookbooks')
46
+ end
47
+
48
+ def launch_stack_monitor(options)
49
+ Output.say_verbose "Starting Stack Monitor..."
50
+ @monitor = Opsicle::Monitor::App.new(@environment, options)
51
+ @monitor.start
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ require 'pathname'
2
+
3
+ module Opsicle
4
+ class S3Bucket
5
+ attr_reader :bucket
6
+
7
+ def initialize(client, bucket_name)
8
+ @bucket = client.s3.buckets[bucket_name]
9
+ raise UnknownBucket unless @bucket.exists?
10
+ end
11
+
12
+ def update(object)
13
+ obj = bucket.objects[object]
14
+ obj.write(Pathname.new(object))
15
+ end
16
+ end
17
+
18
+ UnknownBucket = Class.new(StandardError)
19
+ end
@@ -1,3 +1,3 @@
1
1
  module Opsicle
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency "gli"
23
23
  spec.add_dependency "highline"
24
24
  spec.add_dependency "terminal-table"
25
+ spec.add_dependency "minitar"
25
26
 
26
27
  spec.add_development_dependency "bundler", "~> 1.3"
27
28
  spec.add_development_dependency "rake"
@@ -0,0 +1,129 @@
1
+ require "spec_helper"
2
+ require "opsicle"
3
+ require 'archive/tar/minitar'
4
+
5
+ module Opsicle
6
+ describe ChefUpdate do
7
+ subject { ChefUpdate.new('derp') }
8
+ let(:monitor) { double(:start => nil) }
9
+ let(:client) { double }
10
+ let(:stack) { double(name: 'stack') }
11
+ let(:tar_file) { "stack.tgz" }
12
+ let(:bucket_name) { "cookbooks" }
13
+ let(:bucket) { double }
14
+ let(:cookbooks_dirname) { "cookbooks" }
15
+
16
+ before do
17
+ allow(Client).to receive(:new).with('derp').and_return(client)
18
+ allow(Stack).to receive(:new).with(client).and_return(stack)
19
+ allow(subject).to receive(:tar_file).and_return(tar_file)
20
+
21
+ allow(Monitor::App).to receive(:new).and_return(monitor)
22
+ allow(monitor).to receive(:start)
23
+
24
+ allow(Output).to receive(:say)
25
+ allow(Output).to receive(:say_verbose)
26
+ end
27
+
28
+ context "#execute" do
29
+
30
+ it "tars up the cookooks" do
31
+ allow(subject).to receive(:s3_upload)
32
+ allow(subject).to receive(:cleanup_tar)
33
+ allow(subject).to receive(:update_custom_cookbooks)
34
+ allow(subject).to receive(:launch_stack_monitor)
35
+ expect(subject).to receive(:tar_cookbooks)
36
+
37
+ subject.execute
38
+ end
39
+
40
+ it "uploads the cookbooks to s3" do
41
+ allow(subject).to receive(:tar_cookbooks)
42
+ allow(subject).to receive(:cleanup_tar)
43
+ allow(subject).to receive(:update_custom_cookbooks)
44
+ allow(subject).to receive(:launch_stack_monitor)
45
+ expect(subject).to receive(:s3_upload)
46
+
47
+ subject.execute
48
+ end
49
+
50
+ it "cleans up the tarball created to upload to s3" do
51
+ allow(subject).to receive(:tar_cookbooks)
52
+ allow(subject).to receive(:s3_upload)
53
+ allow(subject).to receive(:update_custom_cookbooks)
54
+ allow(subject).to receive(:launch_stack_monitor)
55
+ expect(subject).to receive(:cleanup_tar)
56
+
57
+ subject.execute
58
+ end
59
+
60
+ it "creates a new update_custom_cookbooks and opens stack monitor" do
61
+ allow(subject).to receive(:tar_cookbooks)
62
+ allow(subject).to receive(:s3_upload)
63
+ allow(subject).to receive(:cleanup_tar)
64
+ allow(subject).to receive(:launch_stack_monitor)
65
+ expect(subject).to receive(:update_custom_cookbooks)
66
+
67
+ subject.execute
68
+ end
69
+
70
+ it "starts the stack monitor" do
71
+ allow(subject).to receive(:tar_cookbooks)
72
+ allow(subject).to receive(:s3_upload)
73
+ allow(subject).to receive(:cleanup_tar)
74
+ allow(subject).to receive(:update_custom_cookbooks)
75
+ expect(subject).to receive(:launch_stack_monitor)
76
+
77
+ subject.execute
78
+ end
79
+ end
80
+
81
+ context "#tar_cookbooks" do
82
+ let(:tar_file_handle) { double }
83
+ let(:cookbooks_dir) { double }
84
+ let(:gzip) { double }
85
+ let(:entries) { %w(recipes templates files) }
86
+ it "tarballs up the cookbooks directory" do
87
+ expect(File).to receive(:open).with(tar_file, 'wb').and_return(tar_file_handle)
88
+ expect(Zlib::GzipWriter).to receive(:new).with(tar_file_handle).and_return(gzip)
89
+ expect(Dir).to receive(:"[]").with(cookbooks_dirname).and_return(cookbooks_dir)
90
+ expect(cookbooks_dir).to receive(:entries).and_return(entries)
91
+ expect(Archive::Tar::Minitar).to receive(:pack).with(entries, gzip)
92
+ subject.send :tar_cookbooks, cookbooks_dirname
93
+ end
94
+ end
95
+
96
+ context "#s3_upload" do
97
+ it "updates the s3 bucket with the tarballed cookbooks" do
98
+ expect(S3Bucket).to receive(:new).with(client, bucket_name).and_return(bucket)
99
+ expect(bucket).to receive(:update).with(tar_file)
100
+ subject.send :s3_upload, bucket_name
101
+ end
102
+ end
103
+
104
+ context "#cleanup_tar" do
105
+ it "removes the tar file now that the upload is complete" do
106
+ expect(FileUtils).to receive(:rm).with(tar_file)
107
+ subject.send :cleanup_tar
108
+ end
109
+ end
110
+
111
+ context "#update_custom_cookbooks" do
112
+ it "runs the aws update custom cookbooks command" do
113
+ expect(client).to receive(:run_command).with('update_custom_cookbooks')
114
+
115
+ subject.send :update_custom_cookbooks
116
+ end
117
+ end
118
+
119
+ context "#launch_stack_monitor" do
120
+ let(:options) { { derp: 'herp' } }
121
+ it "launches the opsicle stack monitor" do
122
+
123
+ expect(Monitor::App).to receive(:new).with('derp', options)
124
+
125
+ subject.send :launch_stack_monitor, options
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+ require "opsicle"
3
+
4
+ module Opsicle
5
+ describe S3Bucket do
6
+ subject { S3Bucket.new(client, bucket_name) }
7
+ let(:bucket_name) { "bucket" }
8
+ let(:bucket) { double(exists?: true) }
9
+ let(:buckets) { double(:"[]" => bucket) }
10
+ let(:s3) { double(buckets: buckets) }
11
+ let(:client) { double(s3: s3) }
12
+
13
+ context "#new" do
14
+ subject { S3Bucket }
15
+ it "finds the bucket from s3" do
16
+ expect(client).to receive(:s3).and_return(s3)
17
+ expect(s3).to receive(:buckets).and_return(buckets)
18
+ expect(buckets).to receive(:"[]").with(bucket_name).and_return(bucket)
19
+ subject.new(client, bucket_name)
20
+ end
21
+
22
+ it "throws an error if the bucket can't be found" do
23
+ allow(bucket).to receive(:exists?).and_return(false)
24
+ expect(client).to receive(:s3).and_return(s3)
25
+ expect(s3).to receive(:buckets).and_return(buckets)
26
+ expect(buckets).to receive(:"[]").with(bucket_name).and_return(bucket)
27
+ expect { subject.new(client, bucket_name) }.to raise_error(UnknownBucket)
28
+ end
29
+ end
30
+
31
+ context "#update" do
32
+ let(:object) { double }
33
+ let(:objects) { double(:"[]" => object) }
34
+ before do
35
+ allow(Pathname).to receive(:new)
36
+ end
37
+
38
+ it "finds the object in the s3 bucket" do
39
+ allow(object).to receive(:write)
40
+ expect(bucket).to receive(:objects).and_return(objects)
41
+ subject.update(object)
42
+ end
43
+
44
+ it "writes the new object in the s3 bucket" do
45
+ allow(bucket).to receive(:objects).and_return(objects)
46
+ expect(object).to receive(:write)
47
+ subject.update(object)
48
+ end
49
+ end
50
+ end
51
+ end
metadata CHANGED
@@ -1,18 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opsicle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Andy Fleener
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2014-03-07 00:00:00.000000000 Z
12
+ date: 2014-03-21 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: aws-sdk
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
19
  - - ~>
18
20
  - !ruby/object:Gem::Version
@@ -20,6 +22,7 @@ dependencies:
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
27
  - - ~>
25
28
  - !ruby/object:Gem::Version
@@ -27,6 +30,7 @@ dependencies:
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: gli
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
35
  - - ! '>='
32
36
  - !ruby/object:Gem::Version
@@ -34,6 +38,7 @@ dependencies:
34
38
  type: :runtime
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
43
  - - ! '>='
39
44
  - !ruby/object:Gem::Version
@@ -41,6 +46,7 @@ dependencies:
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: highline
43
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
44
50
  requirements:
45
51
  - - ! '>='
46
52
  - !ruby/object:Gem::Version
@@ -48,6 +54,7 @@ dependencies:
48
54
  type: :runtime
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
51
58
  requirements:
52
59
  - - ! '>='
53
60
  - !ruby/object:Gem::Version
@@ -55,6 +62,7 @@ dependencies:
55
62
  - !ruby/object:Gem::Dependency
56
63
  name: terminal-table
57
64
  requirement: !ruby/object:Gem::Requirement
65
+ none: false
58
66
  requirements:
59
67
  - - ! '>='
60
68
  - !ruby/object:Gem::Version
@@ -62,6 +70,23 @@ dependencies:
62
70
  type: :runtime
63
71
  prerelease: false
64
72
  version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: minitar
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
65
90
  requirements:
66
91
  - - ! '>='
67
92
  - !ruby/object:Gem::Version
@@ -69,6 +94,7 @@ dependencies:
69
94
  - !ruby/object:Gem::Dependency
70
95
  name: bundler
71
96
  requirement: !ruby/object:Gem::Requirement
97
+ none: false
72
98
  requirements:
73
99
  - - ~>
74
100
  - !ruby/object:Gem::Version
@@ -76,6 +102,7 @@ dependencies:
76
102
  type: :development
77
103
  prerelease: false
78
104
  version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
79
106
  requirements:
80
107
  - - ~>
81
108
  - !ruby/object:Gem::Version
@@ -83,6 +110,7 @@ dependencies:
83
110
  - !ruby/object:Gem::Dependency
84
111
  name: rake
85
112
  requirement: !ruby/object:Gem::Requirement
113
+ none: false
86
114
  requirements:
87
115
  - - ! '>='
88
116
  - !ruby/object:Gem::Version
@@ -90,6 +118,7 @@ dependencies:
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
93
122
  requirements:
94
123
  - - ! '>='
95
124
  - !ruby/object:Gem::Version
@@ -97,6 +126,7 @@ dependencies:
97
126
  - !ruby/object:Gem::Dependency
98
127
  name: rspec
99
128
  requirement: !ruby/object:Gem::Requirement
129
+ none: false
100
130
  requirements:
101
131
  - - '='
102
132
  - !ruby/object:Gem::Version
@@ -104,6 +134,7 @@ dependencies:
104
134
  type: :development
105
135
  prerelease: false
106
136
  version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
107
138
  requirements:
108
139
  - - '='
109
140
  - !ruby/object:Gem::Version
@@ -111,6 +142,7 @@ dependencies:
111
142
  - !ruby/object:Gem::Dependency
112
143
  name: guard
113
144
  requirement: !ruby/object:Gem::Requirement
145
+ none: false
114
146
  requirements:
115
147
  - - ! '>='
116
148
  - !ruby/object:Gem::Version
@@ -118,6 +150,7 @@ dependencies:
118
150
  type: :development
119
151
  prerelease: false
120
152
  version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
121
154
  requirements:
122
155
  - - ! '>='
123
156
  - !ruby/object:Gem::Version
@@ -125,6 +158,7 @@ dependencies:
125
158
  - !ruby/object:Gem::Dependency
126
159
  name: guard-rspec
127
160
  requirement: !ruby/object:Gem::Requirement
161
+ none: false
128
162
  requirements:
129
163
  - - ! '>='
130
164
  - !ruby/object:Gem::Version
@@ -132,6 +166,7 @@ dependencies:
132
166
  type: :development
133
167
  prerelease: false
134
168
  version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
135
170
  requirements:
136
171
  - - ! '>='
137
172
  - !ruby/object:Gem::Version
@@ -158,6 +193,7 @@ files:
158
193
  - lib/opsicle.rb
159
194
  - lib/opsicle/client.rb
160
195
  - lib/opsicle/commands.rb
196
+ - lib/opsicle/commands/chef_update.rb
161
197
  - lib/opsicle/commands/deploy.rb
162
198
  - lib/opsicle/commands/list.rb
163
199
  - lib/opsicle/commands/ssh.rb
@@ -177,10 +213,12 @@ files:
177
213
  - lib/opsicle/monitor/subpanel.rb
178
214
  - lib/opsicle/monitor/translatable.rb
179
215
  - lib/opsicle/output.rb
216
+ - lib/opsicle/s3_bucket.rb
180
217
  - lib/opsicle/stack.rb
181
218
  - lib/opsicle/version.rb
182
219
  - opsicle.gemspec
183
220
  - spec/opsicle/client_spec.rb
221
+ - spec/opsicle/commands/chef_update_spec.rb
184
222
  - spec/opsicle/commands/deploy_spec.rb
185
223
  - spec/opsicle/commands/list_spec.rb
186
224
  - spec/opsicle/commands/ssh_key_spec.rb
@@ -191,33 +229,42 @@ files:
191
229
  - spec/opsicle/monitor/screen_spec.rb
192
230
  - spec/opsicle/monitor/spy/deployments_spec.rb
193
231
  - spec/opsicle/monitor/subpanel_spec.rb
232
+ - spec/opsicle/s3_bucket_spec.rb
194
233
  - spec/spec_helper.rb
195
234
  homepage: https://github.com/sportngin/opsicle
196
235
  licenses:
197
236
  - MIT
198
- metadata: {}
199
237
  post_install_message:
200
238
  rdoc_options: []
201
239
  require_paths:
202
240
  - lib
203
241
  required_ruby_version: !ruby/object:Gem::Requirement
242
+ none: false
204
243
  requirements:
205
244
  - - ! '>='
206
245
  - !ruby/object:Gem::Version
207
246
  version: '0'
247
+ segments:
248
+ - 0
249
+ hash: -1300190123240828980
208
250
  required_rubygems_version: !ruby/object:Gem::Requirement
251
+ none: false
209
252
  requirements:
210
253
  - - ! '>='
211
254
  - !ruby/object:Gem::Version
212
255
  version: '0'
256
+ segments:
257
+ - 0
258
+ hash: -1300190123240828980
213
259
  requirements: []
214
260
  rubyforge_project:
215
- rubygems_version: 2.2.1
261
+ rubygems_version: 1.8.25
216
262
  signing_key:
217
- specification_version: 4
263
+ specification_version: 3
218
264
  summary: An opsworks specific abstraction on top of the aws sdk
219
265
  test_files:
220
266
  - spec/opsicle/client_spec.rb
267
+ - spec/opsicle/commands/chef_update_spec.rb
221
268
  - spec/opsicle/commands/deploy_spec.rb
222
269
  - spec/opsicle/commands/list_spec.rb
223
270
  - spec/opsicle/commands/ssh_key_spec.rb
@@ -228,4 +275,5 @@ test_files:
228
275
  - spec/opsicle/monitor/screen_spec.rb
229
276
  - spec/opsicle/monitor/spy/deployments_spec.rb
230
277
  - spec/opsicle/monitor/subpanel_spec.rb
278
+ - spec/opsicle/s3_bucket_spec.rb
231
279
  - spec/spec_helper.rb
checksums.yaml DELETED
@@ -1,15 +0,0 @@
1
- ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NWMyN2Q2YjBkN2FiMTgwNjhjZjg2N2IwZTI3ZGVkYTFjYTdjY2NlMQ==
5
- data.tar.gz: !binary |-
6
- YzJhOGM3MzMwNDBmNDczMTcwYWRkNjFlYTM2NDJmNjVhMWY4OTM4Ng==
7
- SHA512:
8
- metadata.gz: !binary |-
9
- ZTRjZDg3ZjQ4MjQwYTBkZDEzOWZjMTE4ZWU5MGE1NWE1ZTI2OTg0ZjFhNGQ4
10
- ZGQ4MmUwMzJjMzllZDc4Y2M4NjE0YzI3MzA0MzM2ODBjYWFjZGJmMDYwYWE2
11
- YWQ0Y2EwMzI4ZDdjY2NjNTEyZTFlYjM1MTFlYjQ3Mjc3Yjc5YzQ=
12
- data.tar.gz: !binary |-
13
- Y2I5OWMyMTI1YjhkMjg4NjczYjc4YzU5OTQ0MGQ3YzI1MDY3ZDAzY2NmYTkx
14
- MDk3MTc3NzI4NzJiMDUwZWU4MDg1NzRkNTcyODM1MDZjMDY3ODI4NGI1ZWJi
15
- MWQ1MWQyZmNlNmE0YmE0MWEzNzdkOGM5NGMzOWRiMDY1ZjM0MTI=