azure-armrest 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES +22 -0
- data/README.md +7 -3
- data/lib/azure/armrest.rb +11 -5
- data/lib/azure/armrest/armrest_service.rb +14 -6
- data/lib/azure/armrest/billing/usage_service.rb +1 -8
- data/lib/azure/armrest/configuration.rb +48 -16
- data/lib/azure/armrest/insights/diagnostic_service.rb +1 -1
- data/lib/azure/armrest/insights/event_service.rb +1 -13
- data/lib/azure/armrest/insights/metrics_service.rb +1 -4
- data/lib/azure/armrest/resource_group_based_service.rb +2 -2
- data/lib/azure/armrest/resource_group_service.rb +1 -2
- data/lib/azure/armrest/resource_provider_service.rb +2 -2
- data/lib/azure/armrest/resource_service.rb +8 -6
- data/lib/azure/armrest/subscription_service.rb +1 -1
- data/lib/azure/armrest/version.rb +1 -1
- data/lib/azure/armrest/virtual_machine_extension_service.rb +1 -2
- data/lib/azure/armrest/virtual_machine_image_service.rb +1 -9
- data/lib/azure/armrest/virtual_machine_service.rb +11 -24
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a888f86dfd49210ca1ce4232968d2449f8ee1a2
|
4
|
+
data.tar.gz: 8a19fb58fe560e115b0153e843f0eaaf8329c61d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc6c79041f6c46dcad609fd72e2e5c719c2b84cea7e0fc2cf10fd3da4bc789f92bfcdcddf69748fbfe1e9e837fc001ab607c1d6bc38d2ce0752639284f7acbb7
|
7
|
+
data.tar.gz: 55fd98f1b9af93b7e84b6eaef6b98d5814a31531518ceb00c5c617e2242c547d74ab40c3f4d073bab98d2feb0ca5e09cf1292f45a982bf49532f60817426cc90
|
data/CHANGES
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
= 0.4.1 - 16-Dec-2016
|
2
|
+
* The Configuration#environment accessor was added. As of this release you
|
3
|
+
can specify 'usgov' as an option to the constructor, and the appropriate
|
4
|
+
resource and authority endpoints will be used instead of the public ones.
|
5
|
+
* Added the #authority_url and #resource_url accessors to the Configuration
|
6
|
+
class. Use wisely.
|
7
|
+
* The Armrest::COMMON constant was removed because the resource isn't actually
|
8
|
+
constant. Instead use the authority_url or resource_url methods. This was
|
9
|
+
really only meant for internal use anyway.
|
10
|
+
* Added the Armrest::USGOV_RESOURCE and Armrest::USGOV_AUTHORITY constants.
|
11
|
+
* Fixed a bug in the VirtualMachineService#delete_associated_resources where
|
12
|
+
the method would fail if you requested network security group deletion but
|
13
|
+
there was no associated network security group.
|
14
|
+
* Fixed an issue in the ArmrestService#poll method where a 202 response might
|
15
|
+
not actually have a body. In that case, it is treated as success.
|
16
|
+
* Fixed a logic bug in the ArmrestService#wait method, and added the option
|
17
|
+
to specify 0 (infinity) for the wait time.
|
18
|
+
* Added the ArmrestService#log method, and the Configuration.log= method now
|
19
|
+
automatically converts the argument to a Logger instance if it's not already.
|
20
|
+
* The ArmrestService#base_url method was altered to include subscription
|
21
|
+
information.
|
22
|
+
|
1
23
|
= 0.4.0 - 8-Dec-2016
|
2
24
|
* The Configuration constructor no longer requires a subscription ID. However,
|
3
25
|
the presence of a subscription ID is still required for almost all Service
|
data/README.md
CHANGED
@@ -43,9 +43,13 @@ end
|
|
43
43
|
|
44
44
|
## Subscriptions
|
45
45
|
|
46
|
-
As of version 0.
|
47
|
-
|
48
|
-
|
46
|
+
As of version 0.4.0 you a subscription ID is not longer strictly necessary in
|
47
|
+
the Configuration constructor, but almost all service classes require it in
|
48
|
+
their own constructor. Only the SubscriptionService class does not.
|
49
|
+
|
50
|
+
In version 0.3.x the subscription ID was mandatory. Prior to 0.3.x, if you did
|
51
|
+
not provide a subscription ID in your configuration object, then the first
|
52
|
+
subscription ID returned from a REST call would be used.
|
49
53
|
|
50
54
|
## Notes
|
51
55
|
|
data/lib/azure/armrest.rb
CHANGED
@@ -11,14 +11,20 @@ module Azure
|
|
11
11
|
# The Armrest module mostly serves as a namespace, but also contains any
|
12
12
|
# common constants shared by subclasses.
|
13
13
|
module Armrest
|
14
|
-
# The default Azure resource
|
14
|
+
# The default (public) Azure resource
|
15
15
|
RESOURCE = "https://management.azure.com/"
|
16
16
|
|
17
|
-
# The
|
18
|
-
|
17
|
+
# The resource for US Government clients
|
18
|
+
USGOV_RESOURCE = "https://management.core.usgovcloudapi.net/"
|
19
19
|
|
20
|
-
#
|
21
|
-
|
20
|
+
# The default (public) authority resource
|
21
|
+
AUTHORITY = "https://login.microsoftonline.com/"
|
22
|
+
|
23
|
+
# The authority for US Government clients
|
24
|
+
USGOV_AUTHORITY = "https://login-us.microsoftonline.com/"
|
25
|
+
|
26
|
+
# Environment string used to indicate US Government
|
27
|
+
USGOV_ENVIRONMENT = 'usgov'
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
@@ -12,7 +12,7 @@ module Azure
|
|
12
12
|
|
13
13
|
alias configuration armrest_configuration
|
14
14
|
|
15
|
-
# Base url used for REST calls.
|
15
|
+
# Base url with subscription information used for most REST calls.
|
16
16
|
attr_accessor :base_url
|
17
17
|
|
18
18
|
# Provider for service specific API calls
|
@@ -46,7 +46,7 @@ module Azure
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# Base URL used for REST calls. Modify within method calls as needed.
|
49
|
-
@base_url =
|
49
|
+
@base_url = File.join(configuration.resource_url, 'subscriptions', configuration.subscription_id)
|
50
50
|
|
51
51
|
set_service_api_version(options, service_name)
|
52
52
|
end
|
@@ -165,7 +165,11 @@ module Azure
|
|
165
165
|
def poll(response)
|
166
166
|
return 'Succeeded' if [200, 201].include?(response.response_code)
|
167
167
|
url = response.try(:azure_asyncoperation) || response.try(:location)
|
168
|
-
|
168
|
+
response = rest_get(url).body
|
169
|
+
unless response.blank?
|
170
|
+
status = JSON.parse(response)['status']
|
171
|
+
end
|
172
|
+
status || 'Succeeded' # assume succeeded otherwise the wait method may hang
|
169
173
|
end
|
170
174
|
|
171
175
|
# Wait for the given +response+ to return a status of 'Succeeded', up
|
@@ -175,7 +179,7 @@ module Azure
|
|
175
179
|
#
|
176
180
|
# Internally this will poll the response header every :retry_after
|
177
181
|
# seconds (or 10 seconds if that header isn't found), up to a maximum of
|
178
|
-
# 60 seconds by default.
|
182
|
+
# 60 seconds by default. There is no timeout limit if +max_time+ is 0.
|
179
183
|
#
|
180
184
|
# For most resources the +max_time+ argument should be more than sufficient.
|
181
185
|
# Certain resources, such as virtual machines, could take longer.
|
@@ -184,9 +188,9 @@ module Azure
|
|
184
188
|
sleep_time = response.respond_to?(:retry_after) ? response.retry_after.to_i : 10
|
185
189
|
total_time = 0
|
186
190
|
|
187
|
-
|
191
|
+
until (status = poll(response)) =~ /^succe/i # success or succeeded
|
188
192
|
total_time += sleep_time
|
189
|
-
break if total_time >= max_time
|
193
|
+
break if max_time > 0 && total_time >= max_time
|
190
194
|
sleep sleep_time
|
191
195
|
end
|
192
196
|
|
@@ -353,6 +357,10 @@ module Azure
|
|
353
357
|
def model_class
|
354
358
|
@model_class ||= Object.const_get(self.class.to_s.sub(/Service$/, ''))
|
355
359
|
end
|
360
|
+
|
361
|
+
def log(level = "info", msg)
|
362
|
+
RestClient.log.try(level, msg)
|
363
|
+
end
|
356
364
|
end # ArmrestService
|
357
365
|
end # Armrest
|
358
366
|
end # Azure
|
@@ -59,14 +59,7 @@ module Azure
|
|
59
59
|
private
|
60
60
|
|
61
61
|
def build_url(options = {})
|
62
|
-
url = File.join(
|
63
|
-
Azure::Armrest::COMMON_URI,
|
64
|
-
configuration.subscription_id,
|
65
|
-
'providers',
|
66
|
-
@provider,
|
67
|
-
'UsageAggregates'
|
68
|
-
)
|
69
|
-
|
62
|
+
url = File.join(base_url, 'providers', @provider, 'UsageAggregates')
|
70
63
|
url << "?api-version=#{@api_version}"
|
71
64
|
|
72
65
|
options.each do |key, value|
|
@@ -64,6 +64,15 @@ module Azure
|
|
64
64
|
# Maximum number of threads to use within methods that use Parallel for thread pooling.
|
65
65
|
attr_accessor :max_threads
|
66
66
|
|
67
|
+
# The environment in which to acquire your token.
|
68
|
+
attr_reader :environment
|
69
|
+
|
70
|
+
# The authority URL used to acquire a valid token.
|
71
|
+
attr_accessor :resource_url
|
72
|
+
|
73
|
+
# The resource URL used to acquire a valid token.
|
74
|
+
attr_accessor :authority_url
|
75
|
+
|
67
76
|
# Yields a new Azure::Armrest::Configuration objects. Note that you must
|
68
77
|
# specify a client_id, client_key, tenant_id. The subscription_id is optional
|
69
78
|
# but should be specified in most cases. All other parameters are optional.
|
@@ -90,13 +99,15 @@ module Azure
|
|
90
99
|
def initialize(args)
|
91
100
|
# Use defaults, and override with provided arguments
|
92
101
|
options = {
|
93
|
-
:api_version
|
94
|
-
:accept
|
95
|
-
:content_type
|
96
|
-
:grant_type
|
97
|
-
:proxy
|
98
|
-
:ssl_version
|
99
|
-
:max_threads
|
102
|
+
:api_version => '2015-01-01',
|
103
|
+
:accept => 'application/json',
|
104
|
+
:content_type => 'application/json',
|
105
|
+
:grant_type => 'client_credentials',
|
106
|
+
:proxy => ENV['http_proxy'],
|
107
|
+
:ssl_version => 'TLSv1',
|
108
|
+
:max_threads => 10,
|
109
|
+
:authority_url => Azure::Armrest::AUTHORITY,
|
110
|
+
:resource_url => Azure::Armrest::RESOURCE
|
100
111
|
}.merge(args.symbolize_keys)
|
101
112
|
|
102
113
|
# Avoid thread safety issues for VCR testing.
|
@@ -184,19 +195,18 @@ module Azure
|
|
184
195
|
end
|
185
196
|
end
|
186
197
|
|
187
|
-
#
|
188
|
-
|
189
|
-
# We have to do a little extra work here to convert a possible
|
190
|
-
# file handle to a file name.
|
198
|
+
# Returns the logger instance. It might be initially set through a log
|
199
|
+
# file path, file handler, or already a logger instance.
|
191
200
|
#
|
192
201
|
def self.log
|
193
|
-
|
194
|
-
file || RestClient.log
|
202
|
+
RestClient.log
|
195
203
|
end
|
196
204
|
|
197
|
-
# Sets the log to +output+, which can be a file
|
205
|
+
# Sets the log to +output+, which can be a file, a file handle, or
|
206
|
+
# a logger instance
|
198
207
|
#
|
199
208
|
def self.log=(output)
|
209
|
+
output = Logger.new(output) unless output.kind_of?(Logger)
|
200
210
|
RestClient.log = output
|
201
211
|
end
|
202
212
|
|
@@ -208,6 +218,28 @@ module Azure
|
|
208
218
|
|
209
219
|
private
|
210
220
|
|
221
|
+
# Sets the environment to authenticate against. The environment
|
222
|
+
# must support ActiveDirectory.
|
223
|
+
#
|
224
|
+
def environment=(env)
|
225
|
+
return if env == environment
|
226
|
+
set_auth_and_resource_urls(env)
|
227
|
+
@environment = env
|
228
|
+
end
|
229
|
+
|
230
|
+
# Sets the authority_url and resource_url accessors depending on the
|
231
|
+
# environment.
|
232
|
+
#--
|
233
|
+
# Only two supported at the moment, but more likely to be added.
|
234
|
+
#
|
235
|
+
def set_auth_and_resource_urls(env)
|
236
|
+
case env.to_s.downcase
|
237
|
+
when Azure::Armrest::USGOV_ENVIRONMENT
|
238
|
+
@authority_url = Azure::Armrest::USGOV_AUTHORITY
|
239
|
+
@resource_url = Azure::Armrest::USGOV_RESOURCE
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
211
243
|
# Validate the subscription ID for the given credentials. Returns the
|
212
244
|
# subscription ID if valid.
|
213
245
|
#
|
@@ -275,7 +307,7 @@ module Azure
|
|
275
307
|
end
|
276
308
|
|
277
309
|
def fetch_token
|
278
|
-
token_url = File.join(
|
310
|
+
token_url = File.join(authority_url, tenant_id, 'oauth2/token')
|
279
311
|
|
280
312
|
response = JSON.parse(
|
281
313
|
ArmrestService.send(
|
@@ -288,7 +320,7 @@ module Azure
|
|
288
320
|
:grant_type => grant_type,
|
289
321
|
:client_id => client_id,
|
290
322
|
:client_secret => client_key,
|
291
|
-
:resource =>
|
323
|
+
:resource => resource_url
|
292
324
|
}
|
293
325
|
)
|
294
326
|
)
|
@@ -66,19 +66,7 @@ module Azure
|
|
66
66
|
private
|
67
67
|
|
68
68
|
def build_url(options = {})
|
69
|
-
|
70
|
-
|
71
|
-
url =
|
72
|
-
File.join(
|
73
|
-
Azure::Armrest::COMMON_URI,
|
74
|
-
sub_id,
|
75
|
-
'providers',
|
76
|
-
provider,
|
77
|
-
'eventtypes',
|
78
|
-
'management',
|
79
|
-
'values'
|
80
|
-
)
|
81
|
-
|
69
|
+
url = File.join(base_url, 'providers', provider, 'eventtypes', 'management', 'values')
|
82
70
|
url << "?api-version=#{@api_version}"
|
83
71
|
url << "&$filter=#{options[:filter]}" if options[:filter]
|
84
72
|
url << "&$select=#{options[:select]}" if options[:select]
|
@@ -34,11 +34,8 @@ module Azure
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def build_url(provider, resource_type, resource_name, resource_group, options)
|
37
|
-
sub_id = configuration.subscription_id
|
38
|
-
|
39
37
|
url = File.join(
|
40
|
-
|
41
|
-
sub_id,
|
38
|
+
base_url,
|
42
39
|
'resourceGroups',
|
43
40
|
resource_group,
|
44
41
|
'providers',
|
@@ -114,7 +114,7 @@ module Azure
|
|
114
114
|
api_version ||= configuration.api_version
|
115
115
|
service_name = info['subservice_name'] || info['service_name']
|
116
116
|
|
117
|
-
url = File.join(
|
117
|
+
url = File.join(configuration.resource_url, id_string) + "?api-version=#{api_version}"
|
118
118
|
|
119
119
|
model_class = SERVICE_NAME_MAP.fetch(service_name.downcase) do
|
120
120
|
raise ArgumentError, "unable to map service name #{service_name} to model"
|
@@ -203,7 +203,7 @@ module Azure
|
|
203
203
|
# arguments provided, and appends it with the api_version.
|
204
204
|
#
|
205
205
|
def build_url(resource_group = nil, *args)
|
206
|
-
url =
|
206
|
+
url = base_url
|
207
207
|
url = File.join(url, 'resourceGroups', resource_group) if resource_group
|
208
208
|
url = File.join(url, 'providers', @provider, @service_name)
|
209
209
|
url = File.join(url, *args) unless args.empty?
|
@@ -69,8 +69,7 @@ module Azure
|
|
69
69
|
private
|
70
70
|
|
71
71
|
def build_url(group = nil, *args)
|
72
|
-
|
73
|
-
url = File.join(Azure::Armrest::COMMON_URI, id, 'resourcegroups')
|
72
|
+
url = File.join(base_url, 'resourcegroups')
|
74
73
|
url = File.join(url, group) if group
|
75
74
|
url = File.join(url, *args) unless args.empty?
|
76
75
|
url << "?api-version=#{@api_version}"
|
@@ -61,7 +61,7 @@ module Azure
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def _list_all
|
64
|
-
url = File.join(
|
64
|
+
url = File.join(configuration.resource_url, 'providers')
|
65
65
|
url << "?api-version=#{@api_version}"
|
66
66
|
response = rest_get(url)
|
67
67
|
JSON.parse(response)['value']
|
@@ -127,7 +127,7 @@ module Azure
|
|
127
127
|
|
128
128
|
def build_url(namespace = nil, *args)
|
129
129
|
id = configuration.subscription_id
|
130
|
-
url = File.join(
|
130
|
+
url = File.join(base_url, 'providers')
|
131
131
|
url = File.join(url, namespace) if namespace
|
132
132
|
url = File.join(url, *args) unless args.empty?
|
133
133
|
url << "?api-version=#{@api_version}"
|
@@ -56,8 +56,12 @@ module Azure
|
|
56
56
|
#
|
57
57
|
def move(source_group, source_subscription = configuration.subscription_id)
|
58
58
|
url = File.join(
|
59
|
-
|
60
|
-
'
|
59
|
+
configuration.resource_url,
|
60
|
+
'subscriptions',
|
61
|
+
source_subscription,
|
62
|
+
'resourcegroups',
|
63
|
+
source_group,
|
64
|
+
'moveresources'
|
61
65
|
)
|
62
66
|
|
63
67
|
url << "?api-version=#{@api_version}"
|
@@ -90,12 +94,10 @@ module Azure
|
|
90
94
|
private
|
91
95
|
|
92
96
|
def build_url(resource_group = nil, options = {})
|
93
|
-
url = File.join(Azure::Armrest::COMMON_URI, configuration.subscription_id)
|
94
|
-
|
95
97
|
if resource_group
|
96
|
-
url = File.join(
|
98
|
+
url = File.join(base_url, 'resourceGroups', resource_group, 'resources')
|
97
99
|
else
|
98
|
-
url = File.join(
|
100
|
+
url = File.join(base_url, 'resources')
|
99
101
|
end
|
100
102
|
|
101
103
|
url << "?api-version=#{@api_version}"
|
@@ -98,15 +98,7 @@ module Azure
|
|
98
98
|
# arguments provided, and appends it with the api_version.
|
99
99
|
#
|
100
100
|
def build_url(location, *args)
|
101
|
-
url = File.join(
|
102
|
-
Azure::Armrest::COMMON_URI,
|
103
|
-
configuration.subscription_id,
|
104
|
-
'providers',
|
105
|
-
provider,
|
106
|
-
'locations',
|
107
|
-
location
|
108
|
-
)
|
109
|
-
|
101
|
+
url = File.join(base_url, 'providers', provider, 'locations', location)
|
110
102
|
url = File.join(url, *args) unless args.empty?
|
111
103
|
url << "?api-version=#{@api_version}"
|
112
104
|
end
|
@@ -200,9 +200,10 @@ module Azure
|
|
200
200
|
end
|
201
201
|
|
202
202
|
if options[:network_security_groups]
|
203
|
-
nic.properties.network_security_group
|
204
|
-
|
205
|
-
|
203
|
+
if nic.properties.respond_to?(:network_security_group)
|
204
|
+
nsg = get_associated_resource(nic.properties.network_security_group.id)
|
205
|
+
delete_and_wait(nsgs, nsg.name, nsg.resource_group, options)
|
206
|
+
end
|
206
207
|
end
|
207
208
|
end
|
208
209
|
end
|
@@ -243,13 +244,13 @@ module Azure
|
|
243
244
|
|
244
245
|
# In the unlikely event it did not unlock, just log and skip.
|
245
246
|
if disk.x_ms_lease_status.casecmp('unlocked') != 0
|
246
|
-
|
247
|
+
log('warn', "Unable to delete disk #{disk.container}/#{disk.name}")
|
247
248
|
return
|
248
249
|
end
|
249
250
|
end
|
250
251
|
|
251
252
|
storage_account.delete_blob(disk.container, disk.name, key)
|
252
|
-
|
253
|
+
log("Deleted blob #{disk.container}/#{disk.name}") if options[:verbose]
|
253
254
|
|
254
255
|
begin
|
255
256
|
status_file = File.basename(disk.name, '.vhd') + '.status'
|
@@ -257,7 +258,7 @@ module Azure
|
|
257
258
|
rescue Azure::Armrest::NotFoundException
|
258
259
|
# Ignore, does not always exist.
|
259
260
|
else
|
260
|
-
|
261
|
+
log("Deleted blob #{disk.container}/#{status_file}") if options[:verbose]
|
261
262
|
end
|
262
263
|
end
|
263
264
|
end
|
@@ -271,20 +272,15 @@ module Azure
|
|
271
272
|
def delete_and_wait(service, name, group, options)
|
272
273
|
resource_type = service.class.to_s.sub('Service', '').split('::').last
|
273
274
|
|
274
|
-
|
275
|
-
|
276
|
-
headers = service.delete(name, group)
|
275
|
+
log("Deleting #{resource_type} #{name}/#{group}") if options[:verbose]
|
277
276
|
|
278
|
-
|
279
|
-
status = wait(headers)
|
280
|
-
break if status.downcase.start_with?('succ') # Succeeded, Success, etc.
|
281
|
-
end
|
277
|
+
wait(service.delete(name, group), 0)
|
282
278
|
|
283
|
-
|
279
|
+
log("Deleted #{resource_type} #{name}/#{group}") if options[:verbose]
|
284
280
|
rescue Azure::Armrest::BadRequestException, Azure::Armrest::PreconditionFailedException => err
|
285
281
|
if options[:verbose]
|
286
282
|
msg = "Unable to delete #{resource_type} #{name}/#{group}, skipping. Message: #{err.message}"
|
287
|
-
|
283
|
+
log('warn', msg)
|
288
284
|
end
|
289
285
|
end
|
290
286
|
|
@@ -296,15 +292,6 @@ module Azure
|
|
296
292
|
rest_post(url)
|
297
293
|
nil
|
298
294
|
end
|
299
|
-
|
300
|
-
# Simple log messager. Use the Configuration.log if defined.
|
301
|
-
def log_message(msg, level = 'info')
|
302
|
-
if Azure::Armrest::Configuration.log
|
303
|
-
Azure::Armrest::Configuration.log.send(level.to_sym, msg)
|
304
|
-
else
|
305
|
-
warn msg
|
306
|
-
end
|
307
|
-
end
|
308
295
|
end
|
309
296
|
end
|
310
297
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: azure-armrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel J. Berger
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2016-12-
|
14
|
+
date: 2016-12-16 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: json
|