ovirt-engine-sdk 4.2.0 → 4.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.adoc +10 -0
- data/ext/ovirtsdk4c/ov_http_client.c +132 -10
- data/ext/ovirtsdk4c/ov_http_client.h +8 -1
- data/lib/ovirtsdk4/connection.rb +5 -7
- data/lib/ovirtsdk4/service.rb +5 -7
- data/lib/ovirtsdk4/version.rb +1 -1
- data/lib/ovirtsdk4/writer.rb +1 -3
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 573f19600decb51717ca7b46538158f0bfe145c9
|
4
|
+
data.tar.gz: a42480a7c2627d263106163ecd835c80f0ef2b53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a8a06fe3746bf4c82c9d18c5d9dce0920f6202857444e830c4e917e2d8bacfc2ac9bdd802402a9049b7df029105837c41f1c0c94e3cd7ffc50750abefd66547
|
7
|
+
data.tar.gz: 97c010a0aee2fa5e496df698790a65be800522c4271db84aba09cf3fe6be1794aec263a0f7607edc22eb8aa48fda629a018e182d9e1d872c716b68626deb1219
|
data/CHANGES.adoc
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
This document describes the relevant changes between releases of the SDK.
|
4
4
|
|
5
|
+
== 4.2.1 / Dec 13 2017
|
6
|
+
|
7
|
+
Bug fixes:
|
8
|
+
|
9
|
+
* Fix handling of the `all_content` parameter
|
10
|
+
https://bugzilla.redhat.com/1525555[#1525555].
|
11
|
+
|
12
|
+
* Limit the number of requests sent to `libcurl`
|
13
|
+
https://bugzilla.redhat.com/1525302[#1525302].
|
14
|
+
|
5
15
|
== 4.2.0 / Dec 4 2017
|
6
16
|
|
7
17
|
No chages, version `4.2.0` is identical to `4.2.0-beta2`, only the
|
@@ -64,16 +64,21 @@ static ID READ_ID;
|
|
64
64
|
static ID STRING_ID;
|
65
65
|
static ID STRING_IO_ID;
|
66
66
|
static ID URI_ID;
|
67
|
+
static ID WARN_ID;
|
68
|
+
static ID WARN_Q_ID;
|
67
69
|
static ID WRITE_ID;
|
68
70
|
|
69
71
|
/* References to classes: */
|
70
|
-
static VALUE URI_CLASS;
|
71
72
|
static VALUE STRING_IO_CLASS;
|
73
|
+
static VALUE URI_CLASS;
|
72
74
|
|
73
75
|
/* Constants: */
|
74
76
|
const char CR = '\x0D';
|
75
77
|
const char LF = '\x0A';
|
76
78
|
|
79
|
+
/* Version of libcurl: */
|
80
|
+
static curl_version_info_data* libcurl_version;
|
81
|
+
|
77
82
|
/* Before version 7.38.0 of libcurl the NEGOTIATE authentication method was named GSSNEGOTIATE: */
|
78
83
|
#ifndef CURLAUTH_NEGOTIATE
|
79
84
|
#define CURLAUTH_NEGOTIATE CURLAUTH_GSSNEGOTIATE
|
@@ -85,6 +90,21 @@ const char LF = '\x0A';
|
|
85
90
|
#define CURLPIPE_HTTP1 1
|
86
91
|
#endif
|
87
92
|
|
93
|
+
/* Define options that may not be available in some versions of libcurl: */
|
94
|
+
#if LIBCURL_VERSION_NUM < 0x071e00 /* 7.30.0 */
|
95
|
+
#define CURLMOPT_MAX_HOST_CONNECTIONS 7
|
96
|
+
#define CURLMOPT_MAX_PIPELINE_LENGTH 8
|
97
|
+
#define CURLMOPT_MAX_TOTAL_CONNECTIONS 13
|
98
|
+
#endif
|
99
|
+
|
100
|
+
#if LIBCURL_VERSION_NUM < 0x070f03 /* 7.16.3 */
|
101
|
+
#define CURLMOPT_MAXCONNECTS 6
|
102
|
+
#endif
|
103
|
+
|
104
|
+
#if LIBCURL_VERSION_NUM < 0x070f00 /* 7.16.0 */
|
105
|
+
#define CURLMOPT_PIPELINING 3
|
106
|
+
#endif
|
107
|
+
|
88
108
|
typedef struct {
|
89
109
|
VALUE io; /* IO */
|
90
110
|
char* ptr;
|
@@ -131,6 +151,22 @@ static void ov_http_client_log_info(VALUE log, const char* format, ...) {
|
|
131
151
|
}
|
132
152
|
}
|
133
153
|
|
154
|
+
static void ov_http_client_log_warn(VALUE log, const char* format, ...) {
|
155
|
+
VALUE enabled;
|
156
|
+
VALUE message;
|
157
|
+
va_list args;
|
158
|
+
|
159
|
+
if (!NIL_P(log)) {
|
160
|
+
enabled = rb_funcall(log, WARN_Q_ID, 0);
|
161
|
+
if (RTEST(enabled)) {
|
162
|
+
va_start(args, format);
|
163
|
+
message = rb_vsprintf(format, args);
|
164
|
+
rb_funcall(log, WARN_ID, 1, message);
|
165
|
+
va_end(args);
|
166
|
+
}
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
134
170
|
static void ov_http_client_check_closed(ov_http_client_object* object) {
|
135
171
|
if (object->handle == NULL) {
|
136
172
|
rb_raise(ov_error_class, "The client is already closed");
|
@@ -142,6 +178,7 @@ static void ov_http_client_mark(void* vptr) {
|
|
142
178
|
|
143
179
|
ptr = vptr;
|
144
180
|
rb_gc_mark(ptr->log);
|
181
|
+
rb_gc_mark(ptr->queue);
|
145
182
|
rb_gc_mark(ptr->pending);
|
146
183
|
rb_gc_mark(ptr->completed);
|
147
184
|
}
|
@@ -192,6 +229,8 @@ static VALUE ov_http_client_alloc(VALUE klass) {
|
|
192
229
|
ptr->handle = NULL;
|
193
230
|
ptr->share = NULL;
|
194
231
|
ptr->log = Qnil;
|
232
|
+
ptr->limit = 0;
|
233
|
+
ptr->queue = Qnil;
|
195
234
|
ptr->pending = Qnil;
|
196
235
|
ptr->completed = Qnil;
|
197
236
|
ptr->compress = false;
|
@@ -567,16 +606,22 @@ static VALUE ov_http_client_initialize(int argc, VALUE* argv, VALUE self) {
|
|
567
606
|
Check_Type(opt, T_FIXNUM);
|
568
607
|
pipeline = NUM2LONG(opt);
|
569
608
|
}
|
609
|
+
if (pipeline < 0) {
|
610
|
+
rb_raise(rb_eArgError, "The maximum pipeline length can't be %ld, minimum is 0.", pipeline);
|
611
|
+
}
|
570
612
|
|
571
613
|
/* Get the value of the 'connections' parameter: */
|
572
614
|
opt = rb_hash_aref(opts, CONNECTIONS_SYMBOL);
|
573
615
|
if (NIL_P(opt)) {
|
574
|
-
connections =
|
616
|
+
connections = 1;
|
575
617
|
}
|
576
618
|
else {
|
577
619
|
Check_Type(opt, T_FIXNUM);
|
578
620
|
connections = NUM2LONG(opt);
|
579
621
|
}
|
622
|
+
if (connections < 1) {
|
623
|
+
rb_raise(rb_eArgError, "The maximum number of connections can't be %ld, minimum is 1.", connections);
|
624
|
+
}
|
580
625
|
|
581
626
|
/* Get the value of the 'cookies' parameter. If it is a string it will be used as the path of the file where the
|
582
627
|
cookies will be stored. If it is any other thing it will be treated as a boolean flag indicating if cookies
|
@@ -592,11 +637,22 @@ static VALUE ov_http_client_initialize(int argc, VALUE* argv, VALUE self) {
|
|
592
637
|
ptr->cookies = NULL;
|
593
638
|
}
|
594
639
|
|
640
|
+
/* Create the queue that contains requests that haven't been sent to libcurl yet: */
|
641
|
+
ptr->queue = rb_ary_new();
|
642
|
+
|
595
643
|
/* Create the hash that contains the transfers are pending an completed. Both use the identity of the request
|
596
644
|
as key. */
|
597
645
|
ptr->completed = rb_funcall(rb_hash_new(), COMPARE_BY_IDENTITY_ID, 0);
|
598
646
|
ptr->pending = rb_funcall(rb_hash_new(), COMPARE_BY_IDENTITY_ID, 0);
|
599
647
|
|
648
|
+
/* Calculate the max number of requests that can be handled by libcurl simultaneously. For versions of libcurl
|
649
|
+
newer than 7.30.0 the limit can be increased when using pipelining. For older versions it can't be increased
|
650
|
+
because libcurl would create additional connections for the requests that can't be pipelined. */
|
651
|
+
ptr->limit = connections;
|
652
|
+
if (pipeline > 0 && libcurl_version->version_num >= 0x071e00 /* 7.30.0 */) {
|
653
|
+
ptr->limit *= pipeline;
|
654
|
+
}
|
655
|
+
|
600
656
|
/* Create the libcurl multi handle: */
|
601
657
|
ptr->handle = curl_multi_init();
|
602
658
|
if (ptr->handle == NULL) {
|
@@ -615,14 +671,47 @@ static VALUE ov_http_client_initialize(int argc, VALUE* argv, VALUE self) {
|
|
615
671
|
/* Enable pipelining: */
|
616
672
|
if (pipeline > 0) {
|
617
673
|
curl_multi_setopt(ptr->handle, CURLMOPT_PIPELINING, CURLPIPE_HTTP1);
|
618
|
-
|
619
|
-
|
620
|
-
|
674
|
+
if (libcurl_version->version_num >= 0x071e00 /* 7.30.0 */) {
|
675
|
+
curl_multi_setopt(ptr->handle, CURLMOPT_MAX_PIPELINE_LENGTH, pipeline);
|
676
|
+
}
|
677
|
+
else {
|
678
|
+
ov_http_client_log_warn(
|
679
|
+
ptr->log,
|
680
|
+
"Can't set maximum pipeline length to %d, it isn't supported by libcurl %s. Upgrade to 7.30.0 or "
|
681
|
+
"newer to avoid this issue.",
|
682
|
+
pipeline,
|
683
|
+
libcurl_version->version
|
684
|
+
);
|
685
|
+
}
|
621
686
|
}
|
687
|
+
|
688
|
+
/* Set the max number of connections: */
|
622
689
|
if (connections > 0) {
|
623
|
-
|
624
|
-
|
625
|
-
|
690
|
+
if (libcurl_version->version_num >= 0x071e00 /* 7.30.0 */) {
|
691
|
+
curl_multi_setopt(ptr->handle, CURLMOPT_MAX_HOST_CONNECTIONS, connections);
|
692
|
+
curl_multi_setopt(ptr->handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, connections);
|
693
|
+
}
|
694
|
+
else {
|
695
|
+
ov_http_client_log_warn(
|
696
|
+
ptr->log,
|
697
|
+
"Can't set maximum number of connections to %d, it isn't supported by libcurl %s. Upgrade to 7.30.0 "
|
698
|
+
"or newer to avoid this issue.",
|
699
|
+
connections,
|
700
|
+
libcurl_version->version
|
701
|
+
);
|
702
|
+
}
|
703
|
+
if (libcurl_version->version_num >= 0x070f03 /* 7.16.3 */) {
|
704
|
+
curl_multi_setopt(ptr->handle, CURLMOPT_MAXCONNECTS, connections);
|
705
|
+
}
|
706
|
+
else {
|
707
|
+
ov_http_client_log_warn(
|
708
|
+
ptr->log,
|
709
|
+
"Can't set total maximum connection cache size to %d, it isn't supported by libcurl %s. Upgrade to "
|
710
|
+
"7.16.3 or newer to avoid this issue.",
|
711
|
+
connections,
|
712
|
+
libcurl_version->version
|
713
|
+
);
|
714
|
+
}
|
626
715
|
}
|
627
716
|
|
628
717
|
return self;
|
@@ -922,7 +1011,7 @@ static void ov_http_client_prepare_handle(ov_http_client_object* client_ptr, ov_
|
|
922
1011
|
);
|
923
1012
|
}
|
924
1013
|
|
925
|
-
static VALUE
|
1014
|
+
static VALUE ov_http_client_submit(VALUE self, VALUE request) {
|
926
1015
|
CURL* handle;
|
927
1016
|
VALUE response;
|
928
1017
|
VALUE transfer;
|
@@ -992,7 +1081,26 @@ static VALUE ov_http_client_send(VALUE self, VALUE request) {
|
|
992
1081
|
return Qnil;
|
993
1082
|
}
|
994
1083
|
|
1084
|
+
static VALUE ov_http_client_send(VALUE self, VALUE request) {
|
1085
|
+
ov_http_client_object* ptr;
|
1086
|
+
|
1087
|
+
/* Get the pointer to the native object and check that it isn't closed: */
|
1088
|
+
ov_http_client_ptr(self, ptr);
|
1089
|
+
ov_http_client_check_closed(ptr);
|
1090
|
+
|
1091
|
+
/* If the limit hasn't been reached then submit the request directly to libcurl, otherwise put it in the queue: */
|
1092
|
+
if (RHASH_SIZE(ptr->pending) < ptr->limit) {
|
1093
|
+
ov_http_client_submit(self, request);
|
1094
|
+
}
|
1095
|
+
else {
|
1096
|
+
rb_ary_push(ptr->queue, request);
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
return Qnil;
|
1100
|
+
}
|
1101
|
+
|
995
1102
|
static VALUE ov_http_client_wait(VALUE self, VALUE request) {
|
1103
|
+
VALUE next;
|
996
1104
|
VALUE result;
|
997
1105
|
ov_http_client_object* ptr;
|
998
1106
|
ov_http_client_wait_context context;
|
@@ -1001,15 +1109,24 @@ static VALUE ov_http_client_wait(VALUE self, VALUE request) {
|
|
1001
1109
|
ov_http_client_ptr(self, ptr);
|
1002
1110
|
ov_http_client_check_closed(ptr);
|
1003
1111
|
|
1004
|
-
/* Work till the transfer has been completed
|
1112
|
+
/* Work till the transfer has been completed. */
|
1005
1113
|
context.handle = ptr->handle;
|
1006
1114
|
context.code = CURLE_OK;
|
1007
1115
|
context.cancel = false;
|
1008
1116
|
for (;;) {
|
1117
|
+
/* Move requests from the queue to libcurl: */
|
1118
|
+
while (RARRAY_LEN(ptr->queue) > 0 && RHASH_SIZE(ptr->pending) < ptr->limit) {
|
1119
|
+
next = rb_ary_shift(ptr->queue);
|
1120
|
+
ov_http_client_submit(self, next);
|
1121
|
+
}
|
1122
|
+
|
1123
|
+
/* Check if the response is already available, if so then return it: */
|
1009
1124
|
result = rb_hash_delete(ptr->completed, request);
|
1010
1125
|
if (!NIL_P(result)) {
|
1011
1126
|
return result;
|
1012
1127
|
}
|
1128
|
+
|
1129
|
+
/* If the response isn't available yet, then do some real work: */
|
1013
1130
|
rb_thread_call_without_gvl(
|
1014
1131
|
ov_http_client_wait_task,
|
1015
1132
|
&context,
|
@@ -1082,6 +1199,8 @@ void ov_http_client_define(void) {
|
|
1082
1199
|
STRING_ID = rb_intern("string");
|
1083
1200
|
STRING_IO_ID = rb_intern("StringIO");
|
1084
1201
|
URI_ID = rb_intern("URI");
|
1202
|
+
WARN_ID = rb_intern("warn");
|
1203
|
+
WARN_Q_ID = rb_intern("warn?");
|
1085
1204
|
WRITE_ID = rb_intern("write");
|
1086
1205
|
|
1087
1206
|
/* Locate classes: */
|
@@ -1093,4 +1212,7 @@ void ov_http_client_define(void) {
|
|
1093
1212
|
if (code != CURLE_OK) {
|
1094
1213
|
rb_raise(ov_error_class, "Can't initialize libcurl: %s", curl_easy_strerror(code));
|
1095
1214
|
}
|
1215
|
+
|
1216
|
+
/* Get the libcurl version: */
|
1217
|
+
libcurl_version = curl_version_info(CURLVERSION_NOW);
|
1096
1218
|
}
|
@@ -37,7 +37,14 @@ typedef struct {
|
|
37
37
|
/* The logger: */
|
38
38
|
VALUE log;
|
39
39
|
|
40
|
-
/*
|
40
|
+
/* The max number of requests that can be processed simultaneously by libcurl. Will be calculated multiplying the
|
41
|
+
max number of connections by the pipeline length: */
|
42
|
+
int limit;
|
43
|
+
|
44
|
+
/* This queue contains the requests that have not yet been sent to libcurl for processing: */
|
45
|
+
VALUE queue;
|
46
|
+
|
47
|
+
/* This hash stores the transfers that are pending. The key of the hash is the request that initiated the transfer,
|
41
48
|
and the value is the transfer itself. */
|
42
49
|
VALUE pending;
|
43
50
|
|
data/lib/ovirtsdk4/connection.rb
CHANGED
@@ -101,8 +101,8 @@ module OvirtSDK4
|
|
101
101
|
# as the names of the headers. If the same header is provided here and in the `headers` parameter of a specific
|
102
102
|
# method call, then the `headers` parameter of the specific method call will have precedence.
|
103
103
|
#
|
104
|
-
# @option opts [Integer] :connections (
|
105
|
-
#
|
104
|
+
# @option opts [Integer] :connections (1) The maximum number of connections to open to the host. The value must
|
105
|
+
# be greater than 0.
|
106
106
|
#
|
107
107
|
# @option opts [Integer] :pipeline (0) The maximum number of request to put in an HTTP pipeline without waiting for
|
108
108
|
# the response. If the value is `0` (the default) then pipelining is disabled.
|
@@ -126,7 +126,7 @@ module OvirtSDK4
|
|
126
126
|
@proxy_username = opts[:proxy_username]
|
127
127
|
@proxy_password = opts[:proxy_password]
|
128
128
|
@headers = opts[:headers]
|
129
|
-
@connections = opts[:connections] ||
|
129
|
+
@connections = opts[:connections] || 1
|
130
130
|
@pipeline = opts[:pipeline] || 0
|
131
131
|
|
132
132
|
# Check that the URL has been provided:
|
@@ -275,9 +275,7 @@ module OvirtSDK4
|
|
275
275
|
def follow_link(object)
|
276
276
|
# Check that the "href" has a value, as it is needed in order to retrieve the representation of the object:
|
277
277
|
href = object.href
|
278
|
-
if href.nil?
|
279
|
-
raise Error, "Can't follow link because the 'href' attribute doesn't have a value"
|
280
|
-
end
|
278
|
+
raise Error, "Can't follow link because the 'href' attribute doesn't have a value" if href.nil?
|
281
279
|
|
282
280
|
# Check that the value of the "href" attribute is compatible with the base URL of the connection:
|
283
281
|
prefix = URI(@url).path
|
@@ -634,7 +632,7 @@ module OvirtSDK4
|
|
634
632
|
# parameter. In order to better support those older versions of the engine we need to check if this parameter is
|
635
633
|
# included in the request, and add the corresponding header.
|
636
634
|
unless request.query.nil?
|
637
|
-
all_content = request.query[
|
635
|
+
all_content = request.query[:all_content]
|
638
636
|
request.headers['All-Content'] = all_content unless all_content.nil?
|
639
637
|
end
|
640
638
|
|
data/lib/ovirtsdk4/service.rb
CHANGED
@@ -175,7 +175,7 @@ module OvirtSDK4
|
|
175
175
|
# Get the values of the options specific to this operation:
|
176
176
|
specs.each do |name, kind|
|
177
177
|
value = opts.delete(name)
|
178
|
-
query[name] = Writer.render(value, kind)
|
178
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
179
179
|
end
|
180
180
|
|
181
181
|
# Check the remaining options:
|
@@ -224,7 +224,7 @@ module OvirtSDK4
|
|
224
224
|
# Get the values of the options specific to this operation:
|
225
225
|
specs.each do |name, kind|
|
226
226
|
value = opts.delete(name)
|
227
|
-
query[name] = Writer.render(value, kind)
|
227
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
228
228
|
end
|
229
229
|
|
230
230
|
# Check the remaining options:
|
@@ -274,7 +274,7 @@ module OvirtSDK4
|
|
274
274
|
# Get the values of the options specific to this operation:
|
275
275
|
specs.each do |name, kind|
|
276
276
|
value = opts.delete(name)
|
277
|
-
query[name] = Writer.render(value, kind)
|
277
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
278
278
|
end
|
279
279
|
|
280
280
|
# Check the remaining options:
|
@@ -321,7 +321,7 @@ module OvirtSDK4
|
|
321
321
|
# Get the values of the options specific to this operation:
|
322
322
|
specs.each do |name, kind|
|
323
323
|
value = opts.delete(name)
|
324
|
-
query[name] = Writer.render(value, kind)
|
324
|
+
query[name] = Writer.render(value, kind) unless value.nil?
|
325
325
|
end
|
326
326
|
|
327
327
|
# Check the remaining options:
|
@@ -407,9 +407,7 @@ module OvirtSDK4
|
|
407
407
|
def internal_read_body(response)
|
408
408
|
# First check if the response body is empty, as it makes no sense to check the content type if there is
|
409
409
|
# no body:
|
410
|
-
if response.body.nil? || response.body.length.zero?
|
411
|
-
connection.raise_error(response, 'The response body is empty')
|
412
|
-
end
|
410
|
+
connection.raise_error(response, 'The response body is empty') if response.body.nil? || response.body.length.zero?
|
413
411
|
|
414
412
|
# Check the content type, as otherwise the parsing will fail, and the resulting error message won't be explicit
|
415
413
|
# about the cause of the problem:
|
data/lib/ovirtsdk4/version.rb
CHANGED
data/lib/ovirtsdk4/writer.rb
CHANGED
@@ -195,9 +195,7 @@ module OvirtSDK4
|
|
195
195
|
begin
|
196
196
|
if object.is_a?(Array)
|
197
197
|
# For arrays we can't decide which tag to use, so the 'root' parameter is mandatory in this case:
|
198
|
-
if root.nil?
|
199
|
-
raise Error, "The 'root' option is mandatory when writing arrays"
|
200
|
-
end
|
198
|
+
raise Error, "The 'root' option is mandatory when writing arrays" if root.nil?
|
201
199
|
|
202
200
|
# Write the root tag, and then recursively call the method to write each of the items of the array:
|
203
201
|
cursor.write_start(root)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ovirt-engine-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.2.
|
4
|
+
version: 4.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Juan Hernandez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-12-
|
11
|
+
date: 2017-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -151,7 +151,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
151
|
requirements:
|
152
152
|
- - ">="
|
153
153
|
- !ruby/object:Gem::Version
|
154
|
-
version: '2.
|
154
|
+
version: '2.1'
|
155
155
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ">="
|