chef-provisioning-aws 1.4.0 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/chef/provisioning/aws_driver/credentials2.rb +2 -1
- data/lib/chef/provisioning/aws_driver/driver.rb +53 -6
- data/lib/chef/provisioning/aws_driver/version.rb +1 -1
- data/lib/chef/resource/aws_network_interface.rb +1 -1
- data/lib/chef/resource/aws_subnet.rb +1 -1
- data/spec/integration/machine_spec.rb +261 -0
- metadata +5 -7
- data/spec/acceptance/aws_ebs_volume/nodes/ettores-mbp.lan.json +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e520aad6f6cafb84d380454daf8dd65285e194cd
|
4
|
+
data.tar.gz: b2ec1a747718d0cad0c4bdc9ca6561d1e0daeb6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c3d1891e4c22d14f3b03506f39105a884dae7bc00ad7dc2ace729f5325f7fbc935b29be11631387563529b19efcad224bb0e2447b77a0365dfd5f6969cfcc1c
|
7
|
+
data.tar.gz: a52717acff8c3ccf2945863f616fd8fce022b11fa813b0c8ace68b737be6a797dbb8364cdd94d74884e206357ea786741c657cc1d581ec90502459e8d7e1f6b4
|
@@ -26,7 +26,8 @@ module AWSDriver
|
|
26
26
|
# Try to load the credentials from an ordered list of sources and return the first one that
|
27
27
|
# can be loaded successfully.
|
28
28
|
def get_credentials
|
29
|
-
|
29
|
+
# http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-environment
|
30
|
+
shared_creds = ::Aws::SharedCredentials.new(:profile_name => profile_name, :path => ENV["AWS_CONFIG_FILE"])
|
30
31
|
instance_profile_creds = ::Aws::InstanceProfileCredentials.new(:retries => 1)
|
31
32
|
|
32
33
|
if ENV["AWS_ACCESS_KEY_ID"] && ENV["AWS_SECRET_ACCESS_KEY"]
|
@@ -24,10 +24,11 @@ require 'aws-sdk-v1'
|
|
24
24
|
require 'aws-sdk'
|
25
25
|
require 'retryable'
|
26
26
|
require 'ubuntu_ami'
|
27
|
+
require 'base64'
|
27
28
|
|
28
29
|
# loads the entire aws-sdk
|
29
30
|
AWS.eager_autoload!
|
30
|
-
AWS_V2_SERVICES = {"EC2" => "ec2", "S3" => "s3", "ElasticLoadBalancing" => "elb"}
|
31
|
+
AWS_V2_SERVICES = {"EC2" => "ec2", "S3" => "s3", "ElasticLoadBalancing" => "elb", "IAM" => "iam"}
|
31
32
|
Aws.eager_autoload!(:services => AWS_V2_SERVICES.keys)
|
32
33
|
|
33
34
|
# Need to load the resources after the SDK because `aws_sdk_types` can mess
|
@@ -691,19 +692,65 @@ EOD
|
|
691
692
|
bootstrap_options[:instance_type] ||= default_instance_type
|
692
693
|
image_id = machine_options[:from_image] || bootstrap_options[:image_id] || machine_options[:image_id] || default_ami_for_region(aws_config.region)
|
693
694
|
bootstrap_options[:image_id] = image_id
|
695
|
+
bootstrap_options.delete(:key_path)
|
694
696
|
if !bootstrap_options[:key_name]
|
695
697
|
Chef::Log.debug('No key specified, generating a default one...')
|
696
698
|
bootstrap_options[:key_name] = default_aws_keypair(action_handler, machine_spec)
|
697
699
|
end
|
700
|
+
if bootstrap_options[:iam_instance_profile] && bootstrap_options[:iam_instance_profile].is_a?(String)
|
701
|
+
bootstrap_options[:iam_instance_profile] = {name: bootstrap_options[:iam_instance_profile]}
|
702
|
+
end
|
703
|
+
if bootstrap_options[:user_data]
|
704
|
+
bootstrap_options[:user_data] = Base64.encode64(bootstrap_options[:user_data])
|
705
|
+
end
|
698
706
|
|
699
|
-
|
700
|
-
|
701
|
-
bootstrap_options[:
|
702
|
-
|
703
|
-
|
707
|
+
# V1 -> V2 backwards compatability support
|
708
|
+
unless bootstrap_options.fetch(:monitoring_enabled, nil).nil?
|
709
|
+
bootstrap_options[:monitoring] = {enabled: bootstrap_options.delete(:monitoring_enabled)}
|
710
|
+
end
|
711
|
+
placement = {}
|
712
|
+
if bootstrap_options[:availability_zone]
|
713
|
+
placement[:availability_zone] = bootstrap_options.delete(:availability_zone)
|
714
|
+
end
|
715
|
+
if bootstrap_options[:placement_group]
|
716
|
+
placement[:group_name] = bootstrap_options.delete(:placement_group)
|
717
|
+
end
|
718
|
+
unless bootstrap_options.fetch(:dedicated_tenancy, nil).nil?
|
719
|
+
placement[:tenancy] = bootstrap_options.delete(:dedicated_tenancy) ? "dedicated" : "default"
|
720
|
+
end
|
721
|
+
unless placement.empty?
|
722
|
+
bootstrap_options[:placement] = placement
|
723
|
+
end
|
724
|
+
if bootstrap_options[:subnet]
|
725
|
+
bootstrap_options[:subnet_id] = bootstrap_options.delete(:subnet)
|
704
726
|
end
|
705
727
|
|
706
728
|
bootstrap_options = AWSResource.lookup_options(bootstrap_options, managed_entry_store: machine_spec.managed_entry_store, driver: self)
|
729
|
+
|
730
|
+
# In the migration from V1 to V2 we still support associate_public_ip_address at the top level
|
731
|
+
# we do this after the lookup because we have to copy any present subnets, etc. into the
|
732
|
+
# network interfaces block
|
733
|
+
unless bootstrap_options.fetch(:associate_public_ip_address, nil).nil?
|
734
|
+
if bootstrap_options[:network_interfaces]
|
735
|
+
raise "If you specify network_interfaces you must specify associate_public_ip_address in that list"
|
736
|
+
end
|
737
|
+
network_interface = {
|
738
|
+
:device_index => 0,
|
739
|
+
:associate_public_ip_address => bootstrap_options.delete(:associate_public_ip_address),
|
740
|
+
:delete_on_termination => true
|
741
|
+
}
|
742
|
+
if bootstrap_options[:subnet_id]
|
743
|
+
network_interface[:subnet_id] = bootstrap_options.delete(:subnet_id)
|
744
|
+
end
|
745
|
+
if bootstrap_options[:private_ip_address]
|
746
|
+
network_interface[:private_ip_address] = bootstrap_options.delete(:private_ip_address)
|
747
|
+
end
|
748
|
+
if bootstrap_options[:security_group_ids]
|
749
|
+
network_interface[:groups] = bootstrap_options.delete(:security_group_ids)
|
750
|
+
end
|
751
|
+
bootstrap_options[:network_interfaces] = [network_interface]
|
752
|
+
end
|
753
|
+
|
707
754
|
Chef::Log.debug "AWS Bootstrap options: #{bootstrap_options.inspect}"
|
708
755
|
bootstrap_options
|
709
756
|
end
|
@@ -5,7 +5,7 @@ require 'chef/resource/aws_eip_address'
|
|
5
5
|
class Chef::Resource::AwsNetworkInterface < Chef::Provisioning::AWSDriver::AWSResourceWithEntry
|
6
6
|
include Chef::Provisioning::AWSDriver::AWSTaggable
|
7
7
|
|
8
|
-
aws_sdk_type AWS::EC2::NetworkInterface
|
8
|
+
aws_sdk_type AWS::EC2::NetworkInterface, option_names: []
|
9
9
|
|
10
10
|
attribute :name, kind_of: String, name_attribute: true
|
11
11
|
|
@@ -16,7 +16,7 @@ require 'chef/provisioning/aws_driver/aws_resource_with_entry'
|
|
16
16
|
class Chef::Resource::AwsSubnet < Chef::Provisioning::AWSDriver::AWSResourceWithEntry
|
17
17
|
include Chef::Provisioning::AWSDriver::AWSTaggable
|
18
18
|
|
19
|
-
aws_sdk_type AWS::EC2::Subnet
|
19
|
+
aws_sdk_type AWS::EC2::Subnet, :id => :id
|
20
20
|
|
21
21
|
require 'chef/resource/aws_vpc'
|
22
22
|
require 'chef/resource/aws_network_acl'
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'openssl'
|
2
3
|
|
3
4
|
describe Chef::Resource::Machine do
|
4
5
|
extend AWSSupport
|
@@ -53,6 +54,224 @@ describe Chef::Resource::Machine do
|
|
53
54
|
).and be_idempotent
|
54
55
|
end
|
55
56
|
|
57
|
+
it "base64 encodes the user data", :super_slow do
|
58
|
+
uniq = Random.rand(100)
|
59
|
+
expect_recipe {
|
60
|
+
machine "test_machine_#{uniq}" do
|
61
|
+
machine_options bootstrap_options: {
|
62
|
+
subnet_id: 'test_public_subnet',
|
63
|
+
key_name: 'test_key_pair',
|
64
|
+
user_data: 'echo \'foo\''
|
65
|
+
}
|
66
|
+
action :allocate
|
67
|
+
end
|
68
|
+
}.to create_an_aws_instance("test_machine_#{uniq}"
|
69
|
+
).and be_idempotent
|
70
|
+
expect(
|
71
|
+
driver.ec2_client.describe_instance_attribute(
|
72
|
+
instance_id: driver.ec2_resource.instances(filters: [{name: "tag:Name", values:["test_machine_#{uniq}"]}]).first.id,
|
73
|
+
attribute: "userData"
|
74
|
+
).user_data.value
|
75
|
+
).to eq("ZWNobyAnZm9vJw==\n")
|
76
|
+
end
|
77
|
+
|
78
|
+
it "respects the network_interfaces block with maximum attributes", :super_slow do
|
79
|
+
private_ip_address_start = Random.rand(30)+10
|
80
|
+
expect_recipe {
|
81
|
+
machine "test_machine" do
|
82
|
+
machine_options bootstrap_options: {
|
83
|
+
key_name: 'test_key_pair',
|
84
|
+
instance_type: 'm3.medium',
|
85
|
+
network_interfaces: [
|
86
|
+
{
|
87
|
+
# Cannot set associate_public_ip_address and network_interface_id
|
88
|
+
# network_interface_id: "eth0",
|
89
|
+
device_index: 0,
|
90
|
+
subnet_id: test_public_subnet.aws_object.id,
|
91
|
+
description: "network interface description",
|
92
|
+
private_ip_address: "10.0.0.#{private_ip_address_start}",
|
93
|
+
delete_on_termination: true,
|
94
|
+
groups: [test_security_group.aws_object.id],
|
95
|
+
private_ip_addresses: [
|
96
|
+
{
|
97
|
+
private_ip_address: "10.0.0.#{private_ip_address_start+1}",
|
98
|
+
primary: false
|
99
|
+
},
|
100
|
+
{
|
101
|
+
private_ip_address: "10.0.0.#{private_ip_address_start+2}",
|
102
|
+
primary: false
|
103
|
+
}
|
104
|
+
],
|
105
|
+
# cannot specify both `private_ip_addresses` and `secondary_private_ip_address_count`
|
106
|
+
#secondary_private_ip_address_count: 2,
|
107
|
+
associate_public_ip_address: true
|
108
|
+
}
|
109
|
+
]
|
110
|
+
}
|
111
|
+
action :ready
|
112
|
+
end
|
113
|
+
}.to create_an_aws_instance("test_machine",
|
114
|
+
network_interfaces: [{
|
115
|
+
network_interface_id: /^eni-/,
|
116
|
+
subnet_id: test_public_subnet.aws_object.id,
|
117
|
+
vpc_id: test_vpc.aws_object.id,
|
118
|
+
description: "network interface description",
|
119
|
+
status: "in-use",
|
120
|
+
private_ip_address: "10.0.0.#{private_ip_address_start}",
|
121
|
+
groups: [{group_name: 'test_security_group'}],
|
122
|
+
attachment: {
|
123
|
+
device_index: 0,
|
124
|
+
delete_on_termination: true,
|
125
|
+
status: "attached"
|
126
|
+
},
|
127
|
+
private_ip_addresses: [
|
128
|
+
{
|
129
|
+
private_ip_address: "10.0.0.#{private_ip_address_start}",
|
130
|
+
primary: true,
|
131
|
+
# the action must be :ready to give the public ip time to be assigned
|
132
|
+
association: {
|
133
|
+
public_ip: /\d+/
|
134
|
+
}
|
135
|
+
},
|
136
|
+
{
|
137
|
+
private_ip_address: "10.0.0.#{private_ip_address_start+1}",
|
138
|
+
primary: false
|
139
|
+
},
|
140
|
+
{
|
141
|
+
private_ip_address: "10.0.0.#{private_ip_address_start+2}",
|
142
|
+
primary: false
|
143
|
+
}
|
144
|
+
]
|
145
|
+
}]
|
146
|
+
).and be_idempotent
|
147
|
+
end
|
148
|
+
|
149
|
+
it "converts associate_public_ip_address at the top level to the network interface", :super_slow do
|
150
|
+
private_ip_address_start = Random.rand(30)+10
|
151
|
+
expect_recipe {
|
152
|
+
machine "test_machine" do
|
153
|
+
machine_options bootstrap_options: {
|
154
|
+
key_name: 'test_key_pair',
|
155
|
+
instance_type: 'm3.medium',
|
156
|
+
associate_public_ip_address: true,
|
157
|
+
subnet_id: test_public_subnet.aws_object.id,
|
158
|
+
security_group_ids: [test_security_group.aws_object.id],
|
159
|
+
private_ip_address: "10.0.0.#{private_ip_address_start}"
|
160
|
+
}
|
161
|
+
action :ready
|
162
|
+
end
|
163
|
+
}.to create_an_aws_instance("test_machine",
|
164
|
+
network_interfaces: [{
|
165
|
+
network_interface_id: /^eni-/,
|
166
|
+
subnet_id: test_public_subnet.aws_object.id,
|
167
|
+
vpc_id: test_vpc.aws_object.id,
|
168
|
+
status: "in-use",
|
169
|
+
private_ip_address: "10.0.0.#{private_ip_address_start}",
|
170
|
+
groups: [{group_name: 'test_security_group'}],
|
171
|
+
attachment: {
|
172
|
+
device_index: 0,
|
173
|
+
delete_on_termination: true,
|
174
|
+
status: "attached"
|
175
|
+
},
|
176
|
+
private_ip_addresses: [
|
177
|
+
{
|
178
|
+
private_ip_address: "10.0.0.#{private_ip_address_start}",
|
179
|
+
primary: true,
|
180
|
+
association: {
|
181
|
+
public_ip: /\d+/
|
182
|
+
}
|
183
|
+
}
|
184
|
+
]
|
185
|
+
}]
|
186
|
+
).and be_idempotent
|
187
|
+
end
|
188
|
+
|
189
|
+
context "with a placement group" do
|
190
|
+
before(:context) {
|
191
|
+
driver.ec2_client.create_placement_group({
|
192
|
+
group_name: "agroup",
|
193
|
+
strategy: "cluster"
|
194
|
+
})
|
195
|
+
}
|
196
|
+
|
197
|
+
# Must do after the context so we have waited for the instance to terminate
|
198
|
+
after(:context) {
|
199
|
+
driver.ec2_client.delete_placement_group group_name: "agroup"
|
200
|
+
}
|
201
|
+
|
202
|
+
it "converts V1 keys to V2 keys", :super_slow do
|
203
|
+
expect_recipe {
|
204
|
+
machine "test_machine" do
|
205
|
+
machine_options bootstrap_options: {
|
206
|
+
key_name: 'test_key_pair',
|
207
|
+
instance_type: 'm4.large',
|
208
|
+
monitoring_enabled: false,
|
209
|
+
availability_zone: test_public_subnet.aws_object.availability_zone_name,
|
210
|
+
placement_group: "agroup",
|
211
|
+
dedicated_tenancy: false, # cannot do true, was getting API error
|
212
|
+
subnet: 'test_public_subnet'
|
213
|
+
}
|
214
|
+
action :allocate
|
215
|
+
end
|
216
|
+
}.to create_an_aws_instance("test_machine",
|
217
|
+
monitoring: {state: "disabled"},
|
218
|
+
placement: {
|
219
|
+
availability_zone: test_public_subnet.aws_object.availability_zone_name,
|
220
|
+
group_name: "agroup",
|
221
|
+
tenancy: "default",
|
222
|
+
},
|
223
|
+
subnet_id: test_public_subnet.aws_object.id
|
224
|
+
).and be_idempotent
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context "with a custom iam role" do
|
229
|
+
# TODO when we have IAM support, use the resources
|
230
|
+
before(:context) do
|
231
|
+
assume_role_policy_document = '{"Version":"2008-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":["ec2.amazonaws.com"]},"Action":["sts:AssumeRole"]}]}'
|
232
|
+
driver.iam_client.create_role({
|
233
|
+
role_name: "machine_test_custom_role",
|
234
|
+
assume_role_policy_document: assume_role_policy_document
|
235
|
+
}).role
|
236
|
+
driver.iam_client.create_instance_profile({
|
237
|
+
instance_profile_name: "machine_test_custom_role"
|
238
|
+
})
|
239
|
+
driver.iam_client.add_role_to_instance_profile({
|
240
|
+
instance_profile_name: "machine_test_custom_role",
|
241
|
+
role_name: "machine_test_custom_role"
|
242
|
+
})
|
243
|
+
sleep 5 # grrrrrr, the resource should take care of the polling for us
|
244
|
+
end
|
245
|
+
|
246
|
+
after(:context) do
|
247
|
+
driver.iam_client.remove_role_from_instance_profile({
|
248
|
+
instance_profile_name: "machine_test_custom_role",
|
249
|
+
role_name: "machine_test_custom_role"
|
250
|
+
})
|
251
|
+
driver.iam_client.delete_instance_profile({
|
252
|
+
instance_profile_name: "machine_test_custom_role"
|
253
|
+
})
|
254
|
+
driver.iam_client.delete_role({
|
255
|
+
role_name: "machine_test_custom_role"
|
256
|
+
})
|
257
|
+
end
|
258
|
+
|
259
|
+
it "converts iam_instance_profile from a string to a hash", :super_slow do
|
260
|
+
expect_recipe {
|
261
|
+
machine 'test_machine' do
|
262
|
+
machine_options bootstrap_options: {
|
263
|
+
subnet_id: 'test_public_subnet',
|
264
|
+
key_name: 'test_key_pair',
|
265
|
+
iam_instance_profile: "machine_test_custom_role"
|
266
|
+
}
|
267
|
+
action :allocate
|
268
|
+
end
|
269
|
+
}.to create_an_aws_instance('test_machine',
|
270
|
+
iam_instance_profile: {arn: /machine_test_custom_role/}
|
271
|
+
).and be_idempotent
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
56
275
|
it "machine with from_image option is created from correct image", :super_slow do
|
57
276
|
expect_recipe {
|
58
277
|
|
@@ -113,6 +332,48 @@ describe Chef::Resource::Machine do
|
|
113
332
|
end
|
114
333
|
}.converge }.to_not raise_error
|
115
334
|
end
|
335
|
+
|
336
|
+
# https://github.com/chef/chef-provisioning-aws/pull/295
|
337
|
+
context "with a custom key" do
|
338
|
+
let(:private_key) {
|
339
|
+
k = OpenSSL::PKey::RSA.new(2048)
|
340
|
+
f = Pathname.new(private_key_path)
|
341
|
+
f.write(k.to_pem)
|
342
|
+
k
|
343
|
+
}
|
344
|
+
let(:public_key) {private_key.public_key}
|
345
|
+
let(:private_key_path) {
|
346
|
+
Pathname.new(ENV['HOME']).join(".ssh", key_pair_name).expand_path
|
347
|
+
}
|
348
|
+
let(:key_pair_name) { "test_key_pair_#{Random.rand(100)}" }
|
349
|
+
|
350
|
+
before do
|
351
|
+
driver.ec2_client.import_key_pair({
|
352
|
+
key_name: key_pair_name, # required
|
353
|
+
public_key_material: "#{public_key.ssh_type} #{[public_key.to_blob].pack('m0')}", # required
|
354
|
+
})
|
355
|
+
end
|
356
|
+
|
357
|
+
after do
|
358
|
+
driver.ec2_client.delete_key_pair({
|
359
|
+
key_name: key_pair_name, # required
|
360
|
+
})
|
361
|
+
Pathname.new(private_key_path).delete
|
362
|
+
end
|
363
|
+
|
364
|
+
it "strips key_path from the bootstrap options when creating the machine", :super_slow do
|
365
|
+
expect_recipe {
|
366
|
+
machine 'test_machine' do
|
367
|
+
machine_options bootstrap_options: {
|
368
|
+
key_name: key_pair_name,
|
369
|
+
key_path: private_key_path
|
370
|
+
}
|
371
|
+
end
|
372
|
+
}.to create_an_aws_instance('test_machine'
|
373
|
+
).and be_idempotent
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
116
377
|
end
|
117
378
|
end
|
118
379
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chef-provisioning-aws
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.4.
|
4
|
+
version: 1.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Ewart
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09-
|
11
|
+
date: 2015-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chef-provisioning
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.4'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
26
|
+
version: '1.4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: aws-sdk-v1
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -233,7 +233,6 @@ files:
|
|
233
233
|
- lib/chef/resource/aws_subnet.rb
|
234
234
|
- lib/chef/resource/aws_vpc.rb
|
235
235
|
- lib/chef/resource/aws_vpc_peering_connection.rb
|
236
|
-
- spec/acceptance/aws_ebs_volume/nodes/ettores-mbp.lan.json
|
237
236
|
- spec/aws_support.rb
|
238
237
|
- spec/aws_support/aws_resource_run_wrapper.rb
|
239
238
|
- spec/aws_support/deep_matcher.rb
|
@@ -290,9 +289,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
290
289
|
version: '0'
|
291
290
|
requirements: []
|
292
291
|
rubyforge_project:
|
293
|
-
rubygems_version: 2.4.
|
292
|
+
rubygems_version: 2.4.7
|
294
293
|
signing_key:
|
295
294
|
specification_version: 4
|
296
295
|
summary: Provisioner for creating aws containers in Chef Provisioning.
|
297
296
|
test_files: []
|
298
|
-
has_rdoc:
|