Merge branch '19954-permission-dedup-doc'
authorTom Clegg <tom@curii.com>
Fri, 3 Feb 2023 14:50:41 +0000 (09:50 -0500)
committerTom Clegg <tom@curii.com>
Fri, 3 Feb 2023 14:50:41 +0000 (09:50 -0500)
closes #19954

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

15 files changed:
doc/admin/upgrading.html.textile.liquid
doc/api/methods/container_request_lifecycle.svg [new file with mode: 0644]
doc/api/methods/container_requests.html.textile.liquid
doc/architecture/dispatchcloud.html.textile.liquid
doc/sdk/python/cookbook.html.textile.liquid
doc/sdk/python/sdk-python.html.textile.liquid
lib/config/config.default.yml
lib/config/export.go
lib/dispatchcloud/worker/pool.go
sdk/go/arvados/config.go
services/api/app/models/node.rb
services/api/config/arvados_config.rb
services/api/test/unit/node_test.rb [deleted file]
services/fuse/arvados_fuse/__init__.py
services/fuse/tests/test_mount.py

index 4da168293e717c45042fd6f071f6a5c336e061e1..13c91803bbb9841117e60461f240ae1cb9d764bf 100644 (file)
@@ -28,10 +28,16 @@ TODO: extract this information based on git commit messages and generate changel
 <div class="releasenotes">
 </notextile>
 
-h2(#main). development main (as of 2023-01-16)
+h2(#main). development main (as of 2023-01-27)
 
 "previous: Upgrading to 2.5.0":#v2_5_0
 
+h3. Default limit for cloud VM instances
+
+There is a new configuration entry @CloudVMs.MaxInstances@ (default 64) that limits the number of VMs the cloud dispatcher will run at a time. This may need to be adjusted to suit your anticipated workload.
+
+Using the obsolete configuration entry @MaxCloudVMs@, which was previously accepted in config files but not obeyed, will now result in a deprecation warning.
+
 h3. Slow migration on upgrade
 
 This upgrade includes a database schema update (changing an integer column in each table from 32-bit to 64-bit) that may be slow on a large installation. Expect the arvados-api-server package upgrade to take longer than usual.
diff --git a/doc/api/methods/container_request_lifecycle.svg b/doc/api/methods/container_request_lifecycle.svg
new file mode 100644 (file)
index 0000000..06215aa
--- /dev/null
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.43.0 (0)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="748pt" height="818pt"
+ viewBox="0.00 0.00 747.50 818.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 814)">
+<title>%3</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-814 743.5,-814 743.5,4 -4,4"/>
+<!-- invisiblestart -->
+<g id="node1" class="node">
+<title>invisiblestart</title>
+<ellipse fill="none" stroke="white" cx="88" cy="-792" rx="27" ry="18"/>
+</g>
+<!-- uncommitted -->
+<g id="node2" class="node">
+<title>uncommitted</title>
+<polygon fill="lightgrey" stroke="black" points="176,-723 0,-723 0,-685 176,-685 176,-723"/>
+<text text-anchor="start" x="8" y="-707.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="8" y="-692.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Uncommitted</text>
+</g>
+<!-- invisiblestart&#45;&gt;uncommitted -->
+<g id="edge1" class="edge">
+<title>invisiblestart&#45;&gt;uncommitted</title>
+<path fill="none" stroke="navy" d="M88,-773.6C88,-762.06 88,-746.65 88,-733.36"/>
+<polygon fill="navy" stroke="navy" points="91.5,-733.27 88,-723.27 84.5,-733.27 91.5,-733.27"/>
+<text text-anchor="start" x="88" y="-744.8" font-family="Times,serif" font-size="14.00" fill="navy"> &#160;&#160;user creates container request</text>
+</g>
+<!-- committed -->
+<g id="node3" class="node">
+<title>committed</title>
+<polygon fill="white" stroke="black" points="167,-604 9,-604 9,-551 167,-551 167,-604"/>
+<text text-anchor="start" x="17" y="-588.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="17" y="-573.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Committed</text>
+<text text-anchor="start" x="17" y="-558.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;priority&gt;0</text>
+</g>
+<!-- uncommitted&#45;&gt;committed -->
+<g id="edge2" class="edge">
+<title>uncommitted&#45;&gt;committed</title>
+<path fill="none" stroke="navy" d="M88,-684.9C88,-666.53 88,-637.61 88,-614.55"/>
+<polygon fill="navy" stroke="navy" points="91.5,-614.43 88,-604.43 84.5,-614.43 91.5,-614.43"/>
+<text text-anchor="start" x="88" y="-655.8" font-family="Times,serif" font-size="14.00" fill="navy"> &#160;&#160;user updates to</text>
+<text text-anchor="start" x="88" y="-640.8" font-family="Times,serif" font-size="14.00" fill="navy"> &#160;&#160;&#160;&#160;&#160;state=Committed, priority&gt;0</text>
+</g>
+<!-- reused -->
+<g id="node4" class="node">
+<title>reused</title>
+<polygon fill="lightblue" stroke="black" points="739.5,-619 530.5,-619 530.5,-536 739.5,-536 739.5,-619"/>
+<text text-anchor="start" x="538.5" y="-603.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="538.5" y="-588.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Final</text>
+<text text-anchor="start" x="538.5" y="-573.8" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="538.5" y="-558.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Complete</text>
+<text text-anchor="start" x="538.5" y="-543.8" font-family="Times,serif" font-size="14.00">(reused existing container)</text>
+</g>
+<!-- committed&#45;&gt;reused -->
+<g id="edge6" class="edge">
+<title>committed&#45;&gt;reused</title>
+<path fill="none" stroke="black" d="M167.25,-577.5C260.04,-577.5 414.48,-577.5 520.34,-577.5"/>
+<polygon fill="black" stroke="black" points="520.43,-581 530.43,-577.5 520.43,-574 520.43,-581"/>
+<text text-anchor="middle" x="348.75" y="-584.3" font-family="Times,serif" font-size="14.00">Arvados selects an existing container</text>
+</g>
+<!-- queued -->
+<g id="node5" class="node">
+<title>queued</title>
+<polygon fill="white" stroke="black" points="167,-485 9,-485 9,-402 167,-402 167,-485"/>
+<text text-anchor="start" x="17" y="-469.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="17" y="-454.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Committed</text>
+<text text-anchor="start" x="17" y="-439.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;priority&gt;0</text>
+<text text-anchor="start" x="17" y="-424.8" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="17" y="-409.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Queued</text>
+</g>
+<!-- committed&#45;&gt;queued -->
+<g id="edge3" class="edge">
+<title>committed&#45;&gt;queued</title>
+<path fill="none" stroke="black" d="M88,-550.74C88,-534.9 88,-514.07 88,-495.05"/>
+<polygon fill="black" stroke="black" points="91.5,-495.01 88,-485.01 84.5,-495.01 91.5,-495.01"/>
+<text text-anchor="start" x="88" y="-506.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;Arvados creates a new container</text>
+</g>
+<!-- latecancelled -->
+<g id="node7" class="node">
+<title>latecancelled</title>
+<polygon fill="lightblue" stroke="black" points="709,-343.5 561,-343.5 561,-275.5 709,-275.5 709,-343.5"/>
+<text text-anchor="start" x="569" y="-328.3" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="569" y="-313.3" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Final</text>
+<text text-anchor="start" x="569" y="-298.3" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="569" y="-283.3" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Cancelled</text>
+</g>
+<!-- reused&#45;&gt;latecancelled -->
+<!-- locked -->
+<g id="node6" class="node">
+<title>locked</title>
+<polygon fill="white" stroke="black" points="167,-351 9,-351 9,-268 167,-268 167,-351"/>
+<text text-anchor="start" x="17" y="-335.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="17" y="-320.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Committed</text>
+<text text-anchor="start" x="17" y="-305.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;priority&gt;0</text>
+<text text-anchor="start" x="17" y="-290.8" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="17" y="-275.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Locked</text>
+</g>
+<!-- queued&#45;&gt;locked -->
+<g id="edge4" class="edge">
+<title>queued&#45;&gt;locked</title>
+<path fill="none" stroke="black" d="M88,-401.82C88,-389.02 88,-374.73 88,-361.32"/>
+<polygon fill="black" stroke="black" points="91.5,-361.27 88,-351.27 84.5,-361.27 91.5,-361.27"/>
+<text text-anchor="start" x="88" y="-372.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;Arvados is ready to dispatch the container</text>
+</g>
+<!-- queued&#45;&gt;latecancelled -->
+<g id="edge7" class="edge">
+<title>queued&#45;&gt;latecancelled</title>
+<path fill="none" stroke="navy" d="M167.18,-436.67C233.02,-429.8 328.19,-415.07 406,-384 417.33,-379.47 417.89,-374.06 429,-369 467.51,-351.46 512.51,-337.95 550.6,-328.36"/>
+<polygon fill="navy" stroke="navy" points="551.77,-331.68 560.64,-325.88 550.09,-324.88 551.77,-331.68"/>
+<text text-anchor="middle" x="525" y="-372.8" font-family="Times,serif" font-size="14.00" fill="navy">user updates to priority=0</text>
+</g>
+<!-- locked&#45;&gt;latecancelled -->
+<g id="edge8" class="edge">
+<title>locked&#45;&gt;latecancelled</title>
+<path fill="none" stroke="navy" d="M167.25,-309.5C269.4,-309.5 446.28,-309.5 550.79,-309.5"/>
+<polygon fill="navy" stroke="navy" points="550.98,-313 560.98,-309.5 550.98,-306 550.98,-313"/>
+<text text-anchor="middle" x="364" y="-316.3" font-family="Times,serif" font-size="14.00" fill="navy">user updates to priority=0</text>
+</g>
+<!-- running -->
+<g id="node8" class="node">
+<title>running</title>
+<polygon fill="white" stroke="black" points="167,-217 9,-217 9,-134 167,-134 167,-217"/>
+<text text-anchor="start" x="17" y="-201.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="17" y="-186.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Committed</text>
+<text text-anchor="start" x="17" y="-171.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;priority&gt;0</text>
+<text text-anchor="start" x="17" y="-156.8" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="17" y="-141.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Running</text>
+</g>
+<!-- locked&#45;&gt;running -->
+<g id="edge5" class="edge">
+<title>locked&#45;&gt;running</title>
+<path fill="none" stroke="black" d="M88,-267.82C88,-255.02 88,-240.73 88,-227.32"/>
+<polygon fill="black" stroke="black" points="91.5,-227.27 88,-217.27 84.5,-227.27 91.5,-227.27"/>
+<text text-anchor="start" x="88" y="-238.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;Arvados starts the container process</text>
+</g>
+<!-- containerfailed -->
+<g id="node9" class="node">
+<title>containerfailed</title>
+<polygon fill="lightblue" stroke="black" points="709,-217 561,-217 561,-134 709,-134 709,-217"/>
+<text text-anchor="start" x="569" y="-201.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="569" y="-186.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Final</text>
+<text text-anchor="start" x="569" y="-171.8" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="569" y="-156.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Complete</text>
+<text text-anchor="start" x="569" y="-141.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;exit_code≠0</text>
+</g>
+<!-- latecancelled&#45;&gt;containerfailed -->
+<!-- running&#45;&gt;latecancelled -->
+<g id="edge9" class="edge">
+<title>running&#45;&gt;latecancelled</title>
+<path fill="none" stroke="navy" d="M167.03,-191.54C223.51,-202.63 301.19,-218.59 369,-235 430.25,-249.82 498.77,-268.81 550.93,-283.78"/>
+<polygon fill="navy" stroke="navy" points="550.23,-287.22 560.81,-286.62 552.16,-280.49 550.23,-287.22"/>
+<text text-anchor="middle" x="523" y="-238.8" font-family="Times,serif" font-size="14.00" fill="navy">user updates to priority=0</text>
+</g>
+<!-- running&#45;&gt;containerfailed -->
+<g id="edge10" class="edge">
+<title>running&#45;&gt;containerfailed</title>
+<path fill="none" stroke="black" d="M167.25,-175.5C269.4,-175.5 446.28,-175.5 550.79,-175.5"/>
+<polygon fill="black" stroke="black" points="550.98,-179 560.98,-175.5 550.98,-172 550.98,-179"/>
+<text text-anchor="middle" x="364" y="-182.3" font-family="Times,serif" font-size="14.00">container process fails</text>
+</g>
+<!-- containerfinished -->
+<g id="node10" class="node">
+<title>containerfinished</title>
+<polygon fill="lightblue" stroke="black" points="162,-83 14,-83 14,0 162,0 162,-83"/>
+<text text-anchor="start" x="22" y="-67.8" font-family="Times,serif" font-size="14.00">container request:</text>
+<text text-anchor="start" x="22" y="-52.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Final</text>
+<text text-anchor="start" x="22" y="-37.8" font-family="Times,serif" font-size="14.00">container:</text>
+<text text-anchor="start" x="22" y="-22.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;state=Complete</text>
+<text text-anchor="start" x="22" y="-7.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;exit_code=0</text>
+</g>
+<!-- running&#45;&gt;containerfinished -->
+<g id="edge11" class="edge">
+<title>running&#45;&gt;containerfinished</title>
+<path fill="none" stroke="black" d="M88,-133.82C88,-121.02 88,-106.73 88,-93.32"/>
+<polygon fill="black" stroke="black" points="91.5,-93.27 88,-83.27 84.5,-93.27 91.5,-93.27"/>
+<text text-anchor="start" x="88" y="-104.8" font-family="Times,serif" font-size="14.00"> &#160;&#160;container process succeeds</text>
+</g>
+</g>
+</svg>
index d694ec778df2122b98ac2adbd2a3420f3bebf264..5e15df5ba6866742e1df067f62e5bed00da9cd24 100644 (file)
@@ -64,6 +64,59 @@ table(table table-bordered table-condensed).
 |output_properties|hash|User metadata properties to set on the output collection.  The output collection will also have default properties "type" ("intermediate" or "output") and "container_request" (the uuid of container request that produced the collection).|
 |cumulative_cost|number|Estimated cost of the cloud VMs used to satisfy the request, including retried attempts and completed subrequests, but not including reused containers.|0 if container was reused or VM price information was not available.|
 
+h2(#lifecycle). Container request lifecycle
+
+A container request may be created in the Committed state, or created in the Uncommitted state and then moved into the Committed state.
+
+Once a request is in the Committed state, Arvados locates a suitable existing container or schedules a new one. When the assigned container finishes, the request state changes to Final.
+
+A client may cancel a committed request early (before the assigned container finishes) by setting the request priority to zero.
+
+!{max-width:60em;}{{site.baseurl}}/api/methods/container_request_lifecycle.svg!
+{% comment %}
+# svg generated using `graphviz -Tsvg -O`
+digraph {
+    graph [nojustify=true] [labeljust=l]
+
+    invisiblestart [label = ""] [color=white] [group=lifecycle];
+    node [color=black] [fillcolor=white] [style=filled] [shape=box] [nojustify=true];
+    uncommitted [label = "container request:\l   state=Uncommitted\l"] [fillcolor=lightgrey] [group=lifecycle];
+    {
+        rank=same;
+        committed [label = "container request:\l   state=Committed\l   priority>0\l"] [group=lifecycle];
+        reused [label = "container request:\l   state=Final\lcontainer:\l   state=Complete\l(reused existing container)\l"] [fillcolor=lightblue] [group=endstate];
+    }
+    invisiblestart -> uncommitted [label = "   user creates container request\l"] [color=navy] [fontcolor=navy];
+    uncommitted -> committed [label = "   user updates to\l      state=Committed, priority>0\l"] [color=navy] [fontcolor=navy];
+    queued [label = "container request:\l   state=Committed\l   priority>0\lcontainer:\l   state=Queued\l"] [group=lifecycle];
+    committed -> queued [label = "   Arvados creates a new container\l"];
+    {
+        rank=same;
+        locked [label = "container request:\l   state=Committed\l   priority>0\lcontainer:\l   state=Locked\l"] [group=lifecycle];
+        latecancelled [label = "container request:\l   state=Final\lcontainer:\l   state=Cancelled\l"] [fillcolor=lightblue] [group=endstate];
+    }
+    queued -> locked [label = "   Arvados is ready to dispatch the container\l"];
+    {
+        rank=same;
+        running [label = "container request:\l   state=Committed\l   priority>0\lcontainer:\l   state=Running\l"] [group=lifecycle];
+        containerfailed [label = "container request:\l   state=Final\lcontainer:\l   state=Complete\l   exit_code≠0\l"] [fillcolor=lightblue] [group=endstate];
+    }
+    locked -> running [label = "   Arvados starts the container process\l"];
+    containerfinished [label = "container request:\l   state=Final\lcontainer:\l   state=Complete\l   exit_code=0\l"] [fillcolor=lightblue] [group=lifecycle];
+
+    committed -> reused [label = "Arvados selects an existing container"] [constraint=false] [labeldistance=0.5];
+    queued -> latecancelled [label = "user updates to priority=0"] [color=navy] [fontcolor=navy];
+    locked -> latecancelled [label = "user updates to priority=0"] [color=navy] [fontcolor=navy] [constraint=false];
+    running -> latecancelled [label = "user updates to priority=0"] [color=navy] [fontcolor=navy] [constraint=false];
+    running -> containerfailed [label = "container process fails"];
+    running -> containerfinished [label = "   container process succeeds\l"];
+
+    # layout hacks
+    reused -> latecancelled [style=invis];
+    latecancelled -> containerfailed [style=invis];
+}
+{% endcomment %}
+
 h2(#priority). Priority
 
 The @priority@ field has a range of 0-1000.
@@ -99,7 +152,7 @@ h2(#cancel_container). Canceling a container request
 
 A container request may be canceled by setting its priority to 0, using an update call.
 
-When a container request is canceled, it will still reflect the state of the Container it is associated with via the container_uuid attribute. If that Container is being reused by any other container_requests that are still active, i.e., not yet canceled, that Container may continue to run or be scheduled to run by the system in future. However, if no other container_requests are using that Contianer, then the Container will get canceled as well.
+When a container request is canceled, it will still reflect the state of the Container it is associated with via the container_uuid attribute. If that Container is being reused by any other container_requests that are still active, i.e., not yet canceled, that Container may continue to run or be scheduled to run by the system in future. However, if no other container_requests are using that Container, then the Container will get canceled as well.
 
 h2. Methods
 
index ae854fc2e620acb2e8d48ceb1698a5fcde62f0a7..cc3f11d196aba4421a311dc0a414850dd5e75ae6 100644 (file)
@@ -20,7 +20,7 @@ In this diagram, the black edges show interactions involved in starting a VM ins
 !{max-width:40em}{{site.baseurl}}/architecture/dispatchcloud.svg!
 
 {% comment %}
-# svg generated using https://graphviz.it/
+# svg generated using https://dreampuf.github.io/
 digraph {
     subgraph cluster_cloudvm {
         node [color=black] [fillcolor=white] [style=filled];
index 156b7cbad84857fd505a92b73300c7f621d2fa88..f2d087625e662347d44f2b458159c97a926dfe2b 100644 (file)
@@ -162,7 +162,7 @@ In brief, a permission is represented in Arvados as a link object with the follo
 * @tail_uuid@ identifies the user or role group that receives the permission.
 * @head_uuid@ identifies the Arvados object this permission grants access to.
 
-For details, refer to the "Permissions model documentation":{{ site.baseurl }}/api/permission-model.html. Managing permissions is just a matter of ensuring the desired links exist with the standard @create@, @update@, and @delete@ methods.
+For details, refer to the "Permissions model documentation":{{ site.baseurl }}/api/permission-model.html. Managing permissions is just a matter of ensuring the desired links exist using the standard @create@, @update@, and @delete@ methods.
 
 h3(#grant-permission). Grant permission to an object
 
@@ -396,15 +396,15 @@ with collection.open('ExampleFile') as my_file:
 
 h3(#download-a-file-from-a-collection). Download a file from a collection
 
-Once you have a @Collection@ object, the "@Collection.open@ method":{{ site.baseurl }}/sdk/python/arvados/collection.html#arvados.collection.RichCollectionBase.open lets you open files from a collection the same way you would open files from disk using Python's built-in @open@ function. It returns a file-like object that you can use in many of the same ways you would use any other file object. You can pass it as a source to Python's standard "@shutil.copyfileobj@ function":https://docs.python.org/3/library/shutil.html#shutil.copyfileobj to download it. This code downloads @ExampleFile@ from your collection and saves it to the current working directory as @ExampleDownload@:
+Once you have a @Collection@ object, the "@Collection.open@ method":{{ site.baseurl }}/sdk/python/arvados/collection.html#arvados.collection.RichCollectionBase.open lets you open files from a collection the same way you would open files from disk using Python's built-in @open@ function. You pass a second mode argument like @'rb'@ to open the file in binary mode. It returns a file-like object that you can use in many of the same ways you would use any other file object. You can pass it as a source to Python's standard "@shutil.copyfileobj@ function":https://docs.python.org/3/library/shutil.html#shutil.copyfileobj to download it. This code downloads @ExampleFile@ from your collection and saves it to the current working directory as @ExampleDownload@:
 
 {% codeblock as python %}
 import arvados.collection
 import shutil
 collection = arvados.collection.Collection(...)
 with (
-  collection.open('ExampleFile') as src_file,
-  open('ExampleDownload', 'w') as dst_file,
+  collection.open('ExampleFile', 'rb') as src_file,
+  open('ExampleDownload', 'wb') as dst_file,
 ):
     shutil.copyfileobj(src_file, dst_file)
 {% endcodeblock %}
@@ -418,7 +418,7 @@ import arvados.collection
 collection = arvados.collection.Collection(...)
 with collection.open('ExampleFile', 'w') as my_file:
     # Write to my_file as desired.
-    # This example writes "Hello, world!" to the file.
+    # This example writes "Hello, Arvados!" to the file.
     print("Hello, Arvados!", file=my_file)
 collection.save_new(...)  # or collection.save() to update an existing collection
 {% endcodeblock %}
@@ -432,8 +432,8 @@ import arvados.collection
 import shutil
 collection = arvados.collection.Collection(...)
 with (
-  open('ExampleFile') as src_file,
-  collection.open('ExampleUpload', 'w') as dst_file,
+  open('ExampleFile', 'rb') as src_file,
+  collection.open('ExampleUpload', 'wb') as dst_file,
 ):
     shutil.copyfileobj(src_file, dst_file)
 collection.save_new(...)  # or collection.save() to update an existing collection
@@ -616,7 +616,7 @@ for mount_name, mount_source in container_request['mounts'].items():
         pprint.pprint(mount_source.get('content'))
 {% endcodeblock %}
 
-h3(#get-input-of-a-cwl-workflow). Get input of a container or CWL workflow run
+h3(#get-input-of-a-cwl-workflow). Get input of a CWL workflow run
 
 When you run a CWL workflow, the CWL inputs are stored in the container request's @mounts@ field as a JSON mount named @/var/lib/cwl/cwl.input.json@.
 
index bf66194068cb6c2e569f6a6931e0275e82d3cf00..46393069220f6fe3be82954bdba9ad9f147c70d9 100644 (file)
@@ -16,13 +16,15 @@ h2. Installation
 
 If you are logged in to an Arvados VM, the Python SDK should be installed.
 
-To use the Python SDK elsewhere, you can install from PyPI or a distribution package.
+To use the Python SDK elsewhere, you can install it "from an Arvados distribution package":#package-install or "from PyPI using pip":#pip-install.
 
+{% include 'notebox_begin_warning' %}
 As of Arvados 2.2, the Python SDK requires Python 3.6+.  The last version to support Python 2.7 is Arvados 2.0.4.
+{% include 'notebox_end' %}
 
-h2. Option 1: Install from a distribution package
+h2(#package-install). Install from a distribution package
 
-This installation method is recommended to make the CLI tools available system-wide. It can coexist with the installation method described in option 2, below.
+This installation method is recommended to make the CLI tools available system-wide. It can coexist with the pip installation method described below.
 
 First, configure the "Arvados package repositories":../../install/packages.html
 
@@ -30,30 +32,15 @@ First, configure the "Arvados package repositories":../../install/packages.html
 
 {% include 'install_packages' %}
 
-h2. Option 2: Install with pip
-
-This installation method is recommended to use the SDK in your own Python programs. If installed into a @virtualenv@, it can coexist with the system-wide installation method from a distribution package.
-
-Run @pip install arvados-python-client@ in an appropriate installation environment, such as a @virtualenv@.
-
-Note:
-
-The SDK uses @pycurl@ which depends on the @libcurl@ C library.  To build the module you may have to first install additional packages.  On Debian 10 this is:
-
-<pre>
-$ apt-get install git build-essential python3-dev libcurl4-openssl-dev libssl-dev
-</pre>
-
-If your version of @pip@ is 1.4 or newer, the @pip install@ command might give an error: "Could not find a version that satisfies the requirement arvados-python-client". If this happens, try @pip install --pre arvados-python-client@.
-
-h2. Test installation
-
-If the SDK is installed and your @ARVADOS_API_HOST@ and @ARVADOS_API_TOKEN@ environment variables are set up correctly (see "api-tokens":{{site.baseurl}}/user/reference/api-tokens.html for details), @import arvados@ should produce no errors.
+{% include 'notebox_begin_warning' %}
+If you are on Ubuntu 18.04, please note that the Arvados packages that use Python depend on the python-3.8 package. This means they are installed under @/usr/share/python3.8@, not @/usr/share/python3@. You will need to update the commands below accordingly.
+{% include 'notebox_end' %}
 
-If you installed with pip (option 1, above):
+The package includes a virtualenv, which means the correct Python environment needs to be loaded before the Arvados SDK can be imported. You can test the installation by doing that, then creating a client object. Ensure your "@ARVADOS_API_HOST@ and @ARVADOS_API_TOKEN@ credentials are set up correctly":{{site.baseurl}}/user/reference/api-tokens.html. Then you should be able to run the following without any errors:
 
 <notextile>
-<pre>~$ <code class="userinput">python</code>
+<pre>~$ <code class="userinput">source /usr/share/python3/dist/python3-arvados-python-client/bin/activate</code>
+(python-arvados-python-client) ~$ <code class="userinput">python</code>
 Python 3.7.3 (default, Jul 25 2020, 13:03:44)
 [GCC 8.3.0] on linux
 Type "help", "copyright", "credits" or "license" for more information.
@@ -63,15 +50,10 @@ Type "help", "copyright", "credits" or "license" for more information.
 </pre>
 </notextile>
 
-If you installed from a distribution package (option 2): the package includes a virtualenv, which means the correct Python environment needs to be loaded before the Arvados SDK can be imported. This can be done by activating the virtualenv first:
-
-{% include 'notebox_begin_warning' %}
-If you are on Ubuntu 18.04, please note that the Arvados packages that use Python depend on the python-3.8 package. This means they are installed under @/usr/share/python3.8@, not @/usr/share/python3@. You will need to update the commands below accordingly.
-{% include 'notebox_end' %}
+Alternatively, you can run the Python executable inside the @virtualenv@ directly:
 
 <notextile>
-<pre>~$ <code class="userinput">source /usr/share/python3/dist/python3-arvados-python-client/bin/activate</code>
-(python-arvados-python-client) ~$ <code class="userinput">python</code>
+<pre>~$ <code class="userinput">/usr/share/python3/dist/python3-arvados-python-client/bin/python</code>
 Python 3.7.3 (default, Jul 25 2020, 13:03:44)
 [GCC 8.3.0] on linux
 Type "help", "copyright", "credits" or "license" for more information.
@@ -81,10 +63,28 @@ Type "help", "copyright", "credits" or "license" for more information.
 </pre>
 </notextile>
 
-Or alternatively, by using the Python executable from the virtualenv directly:
+After you have successfully tested your installation, proceed to the the "API client overview":api-client.html and "cookbook":cookbook.html to learn how to use the SDK.
+
+h2(#pip-install). Install from PyPI with pip
+
+This installation method is recommended to use the SDK in your own Python programs. If installed into a @virtualenv@, it can coexist with the system-wide installation method from a distribution package.
+
+The SDK uses @pycurl@ which depends on the @libcurl@ C library.  To build the module you may have to first install additional packages.  On Debian 10 you can do this by running:
+
+<pre>
+$ apt-get install git build-essential python3-dev libcurl4-openssl-dev libssl-dev
+</pre>
+
+Run @python3 -m pip install arvados-python-client@ in an appropriate installation environment, such as a @virtualenv@.
+
+{% include 'notebox_begin_warning' %}
+If your version of @pip@ is 1.4 or newer, the @pip install@ command might give an error: "Could not find a version that satisfies the requirement arvados-python-client". If this happens, try @python3 -m pip install --pre arvados-python-client@.
+{% include 'notebox_end' %}
+
+You can test the installation by creating a client object. Ensure your "@ARVADOS_API_HOST@ and @ARVADOS_API_TOKEN@ credentials are set up correctly":{{site.baseurl}}/user/reference/api-tokens.html. Then you should be able to run the following without any errors:
 
 <notextile>
-<pre>~$ <code class="userinput">/usr/share/python3/dist/python3-arvados-python-client/bin/python</code>
+<pre>~$ <code class="userinput">python3</code>
 Python 3.7.3 (default, Jul 25 2020, 13:03:44)
 [GCC 8.3.0] on linux
 Type "help", "copyright", "credits" or "license" for more information.
@@ -94,6 +94,4 @@ Type "help", "copyright", "credits" or "license" for more information.
 </pre>
 </notextile>
 
-h2. Usage
-
-Check out the "API client overview":api-client.html and "cookbook":cookbook.html.
+After you have successfully tested your installation, proceed to the the "API client overview":api-client.html and "cookbook":cookbook.html to learn how to use the SDK.
index 29a4a640b529ac77c79564dd9a7b3c76041c9301..26ada44d6deeaed721bf93823d87dc5f6f381793 100644 (file)
@@ -1005,13 +1005,6 @@ Clusters:
       # with the cancelled container.
       MaxRetryAttempts: 3
 
-      # The maximum number of compute nodes that can be in use simultaneously
-      # If this limit is reduced, any existing nodes with slot number >= new limit
-      # will not be counted against the new limit. In other words, the new limit
-      # won't be strictly enforced until those nodes with higher slot numbers
-      # go down.
-      MaxComputeVMs: 64
-
       # Schedule all child containers on preemptible instances (e.g. AWS
       # Spot Instances) even if not requested by the submitter.
       #
@@ -1327,6 +1320,15 @@ Clusters:
         # providers too, if desired.
         MaxConcurrentInstanceCreateOps: 1
 
+        # The maximum number of instances to run at a time, or 0 for
+        # unlimited.
+        #
+        # If more instances than this are already running and busy
+        # when the dispatcher starts up, the running containers will
+        # be allowed to finish before the excess instances are shut
+        # down.
+        MaxInstances: 64
+
         # Interval between cloud provider syncs/updates ("list all
         # instances").
         SyncInterval: 1m
index bc78644862a61f382d36b87694c7a93e150eeeb7..f9699c6edcf5c5ef74d3a1977341a8b41084aef7 100644 (file)
@@ -131,7 +131,6 @@ var whitelist = map[string]bool{
        "Containers.Logging":                       false,
        "Containers.LogReuseDecisions":             false,
        "Containers.LSF":                           false,
-       "Containers.MaxComputeVMs":                 false,
        "Containers.MaxDispatchAttempts":           false,
        "Containers.MaxRetryAttempts":              true,
        "Containers.MinRetryPeriod":                true,
index 66e0bfee910a236b46980f2db4b7c30850b3a759..3abcba6c7365766cf0c7d38315d47dc1292a03e1 100644 (file)
@@ -111,6 +111,7 @@ func NewPool(logger logrus.FieldLogger, arvClient *arvados.Client, reg *promethe
                instanceTypes:                  cluster.InstanceTypes,
                maxProbesPerSecond:             cluster.Containers.CloudVMs.MaxProbesPerSecond,
                maxConcurrentInstanceCreateOps: cluster.Containers.CloudVMs.MaxConcurrentInstanceCreateOps,
+               maxInstances:                   cluster.Containers.CloudVMs.MaxInstances,
                probeInterval:                  duration(cluster.Containers.CloudVMs.ProbeInterval, defaultProbeInterval),
                syncInterval:                   duration(cluster.Containers.CloudVMs.SyncInterval, defaultSyncInterval),
                timeoutIdle:                    duration(cluster.Containers.CloudVMs.TimeoutIdle, defaultTimeoutIdle),
@@ -155,6 +156,7 @@ type Pool struct {
        probeInterval                  time.Duration
        maxProbesPerSecond             int
        maxConcurrentInstanceCreateOps int
+       maxInstances                   int
        timeoutIdle                    time.Duration
        timeoutBooting                 time.Duration
        timeoutProbe                   time.Duration
@@ -302,10 +304,10 @@ func (wp *Pool) Unallocated() map[arvados.InstanceType]int {
 // pool. The worker is added immediately; instance creation runs in
 // the background.
 //
-// Create returns false if a pre-existing error state prevents it from
-// even attempting to create a new instance. Those errors are logged
-// by the Pool, so the caller does not need to log anything in such
-// cases.
+// Create returns false if a pre-existing error or a configuration
+// setting prevents it from even attempting to create a new
+// instance. Those errors are logged by the Pool, so the caller does
+// not need to log anything in such cases.
 func (wp *Pool) Create(it arvados.InstanceType) bool {
        logger := wp.logger.WithField("InstanceType", it.Name)
        wp.setupOnce.Do(wp.setup)
@@ -315,7 +317,9 @@ func (wp *Pool) Create(it arvados.InstanceType) bool {
        }
        wp.mtx.Lock()
        defer wp.mtx.Unlock()
-       if time.Now().Before(wp.atQuotaUntil) || wp.instanceSet.throttleCreate.Error() != nil {
+       if time.Now().Before(wp.atQuotaUntil) ||
+               wp.instanceSet.throttleCreate.Error() != nil ||
+               (wp.maxInstances > 0 && wp.maxInstances <= len(wp.workers)+len(wp.creating)) {
                return false
        }
        // The maxConcurrentInstanceCreateOps knob throttles the number of node create
@@ -361,15 +365,19 @@ func (wp *Pool) Create(it arvados.InstanceType) bool {
                }
                wp.updateWorker(inst, it)
        }()
+       if len(wp.creating)+len(wp.workers) == wp.maxInstances {
+               logger.Infof("now at MaxInstances limit of %d instances", wp.maxInstances)
+       }
        return true
 }
 
 // AtQuota returns true if Create is not expected to work at the
-// moment.
+// moment (e.g., cloud provider has reported quota errors, or we are
+// already at our own configured quota).
 func (wp *Pool) AtQuota() bool {
        wp.mtx.Lock()
        defer wp.mtx.Unlock()
-       return time.Now().Before(wp.atQuotaUntil)
+       return time.Now().Before(wp.atQuotaUntil) || (wp.maxInstances > 0 && wp.maxInstances <= len(wp.workers)+len(wp.creating))
 }
 
 // SetIdleBehavior determines how the indicated instance will behave
index 76ed7cefba28bf9771349c23c3d983ebea245667..677706c08208541ef4f13a4e1f8430272d9782e5 100644 (file)
@@ -498,7 +498,6 @@ type ContainersConfig struct {
        DefaultKeepCacheRAM           ByteSize
        DispatchPrivateKey            string
        LogReuseDecisions             bool
-       MaxComputeVMs                 int
        MaxDispatchAttempts           int
        MaxRetryAttempts              int
        MinRetryPeriod                Duration
@@ -562,6 +561,7 @@ type CloudVMsConfig struct {
        MaxCloudOpsPerSecond           int
        MaxProbesPerSecond             int
        MaxConcurrentInstanceCreateOps int
+       MaxInstances                   int
        PollInterval                   Duration
        ProbeInterval                  Duration
        SSHPort                        string
index c8b463696bb5423b1d5a5f7f5533b95637246165..c8a606e2b808d63a714d41d362e11e34121eebac 100644 (file)
@@ -24,6 +24,7 @@ class Node < ArvadosModel
   attr_accessor :job_readable
 
   UNUSED_NODE_IP = '127.40.4.0'
+  MAX_VMS = 3
 
   api_accessible :user, :extend => :common do |t|
     t.add :hostname
@@ -159,7 +160,7 @@ class Node < ArvadosModel
                           # query label:
                           'Node.available_slot_number',
                           # [col_id, val] for $1 vars:
-                          [[nil, Rails.configuration.Containers.MaxComputeVMs]],
+                          [[nil, MAX_VMS]],
                          ).rows.first.andand.first
   end
 
@@ -267,7 +268,7 @@ class Node < ArvadosModel
       !Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate.to_s.empty? and
       !Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname.empty?)
 
-    (0..Rails.configuration.Containers.MaxComputeVMs-1).each do |slot_number|
+    (0..MAX_VMS-1).each do |slot_number|
       hostname = hostname_for_slot(slot_number)
       hostfile = File.join Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir, "#{hostname}.conf"
       if !File.exist? hostfile
index c47eeb55146221d0c9a06ce7fcfd006e0dcee626..d928d592c93f7b01f58145a37d08b380941d1720 100644 (file)
@@ -132,7 +132,6 @@ arvcfg.declare_config "Containers.DefaultKeepCacheRAM", Integer, :container_defa
 arvcfg.declare_config "Containers.MaxDispatchAttempts", Integer, :max_container_dispatch_attempts
 arvcfg.declare_config "Containers.MaxRetryAttempts", Integer, :container_count_max
 arvcfg.declare_config "Containers.AlwaysUsePreemptibleInstances", Boolean, :preemptible_instances
-arvcfg.declare_config "Containers.MaxComputeVMs", Integer, :max_compute_nodes
 arvcfg.declare_config "Containers.Logging.LogBytesPerEvent", Integer, :crunch_log_bytes_per_event
 arvcfg.declare_config "Containers.Logging.LogSecondsBetweenEvents", ActiveSupport::Duration, :crunch_log_seconds_between_events
 arvcfg.declare_config "Containers.Logging.LogThrottlePeriod", ActiveSupport::Duration, :crunch_log_throttle_period
diff --git a/services/api/test/unit/node_test.rb b/services/api/test/unit/node_test.rb
deleted file mode 100644 (file)
index 9fa3feb..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-require 'test_helper'
-require 'tmpdir'
-require 'tempfile'
-
-class NodeTest < ActiveSupport::TestCase
-  def ping_node(node_name, ping_data)
-    set_user_from_auth :admin
-    node = nodes(node_name)
-    node.ping({ping_secret: node.info['ping_secret'],
-                ip: node.ip_address}.merge(ping_data))
-    node
-  end
-
-  test "pinging a node can add and update stats" do
-    node = ping_node(:idle, {total_cpu_cores: '12', total_ram_mb: '512'})
-    assert_equal(12, node.properties['total_cpu_cores'])
-    assert_equal(512, node.properties['total_ram_mb'])
-  end
-
-  test "stats disappear if not in a ping" do
-    node = ping_node(:idle, {total_ram_mb: '256'})
-    refute_includes(node.properties, 'total_cpu_cores')
-    assert_equal(256, node.properties['total_ram_mb'])
-  end
-
-  test "worker state is down for node with no slot" do
-    node = nodes(:was_idle_now_down)
-    assert_nil node.slot_number, "fixture is not what I expected"
-    assert_equal 'down', node.crunch_worker_state, "wrong worker state"
-  end
-
-  test "dns_server_conf_template" do
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir = Rails.root.join 'tmp'
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate = Rails.root.join 'config', 'unbound.template'
-    conffile = Rails.root.join 'tmp', 'compute65535.conf'
-    File.unlink conffile rescue nil
-    assert Node.dns_server_update 'compute65535', '127.0.0.1'
-    assert_match(/\"1\.0\.0\.127\.in-addr\.arpa\. IN PTR compute65535\.zzzzz\.arvadosapi\.com\"/, IO.read(conffile))
-    File.unlink conffile
-  end
-
-  test "dns_server_restart_command" do
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir = Rails.root.join 'tmp'
-    Rails.configuration.Containers.SLURM.Managed.DNSServerReloadCommand = 'foobar'
-    restartfile = Rails.root.join 'tmp', 'restart.txt'
-    File.unlink restartfile rescue nil
-    assert Node.dns_server_update 'compute65535', '127.0.0.127'
-    assert_equal "foobar\n", IO.read(restartfile)
-    File.unlink restartfile
-  end
-
-  test "dns_server_restart_command fail" do
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir = Rails.root.join 'tmp', 'bogusdir'
-    Rails.configuration.Containers.SLURM.Managed.DNSServerReloadCommand = 'foobar'
-    refute Node.dns_server_update 'compute65535', '127.0.0.127'
-  end
-
-  test "dns_server_update_command with valid command" do
-    testfile = Rails.root.join('tmp', 'node_test_dns_server_update_command.txt')
-    Rails.configuration.Containers.SLURM.Managed.DNSServerUpdateCommand =
-      ('echo -n "%{hostname} == %{ip_address}" >' +
-       testfile.to_s.shellescape)
-    assert Node.dns_server_update 'compute65535', '127.0.0.1'
-    assert_equal 'compute65535 == 127.0.0.1', IO.read(testfile)
-    File.unlink testfile
-  end
-
-  test "dns_server_update_command with failing command" do
-    Rails.configuration.Containers.SLURM.Managed.DNSServerUpdateCommand = 'false %{hostname}'
-    refute Node.dns_server_update 'compute65535', '127.0.0.1'
-  end
-
-  test "dns update with no commands/dirs configured" do
-    Rails.configuration.Containers.SLURM.Managed.DNSServerUpdateCommand = ""
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir = ""
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate = 'ignored!'
-    Rails.configuration.Containers.SLURM.Managed.DNSServerReloadCommand = 'ignored!'
-    assert Node.dns_server_update 'compute65535', '127.0.0.127'
-  end
-
-  test "don't leave temp files behind if there's an error writing them" do
-    Rails.configuration.Containers.SLURM.Managed.DNSServerConfTemplate = Rails.root.join 'config', 'unbound.template'
-    Tempfile.any_instance.stubs(:puts).raises(IOError)
-    Dir.mktmpdir do |tmpdir|
-      Rails.configuration.Containers.SLURM.Managed.DNSServerConfDir = tmpdir
-      refute Node.dns_server_update 'compute65535', '127.0.0.127'
-      assert_empty Dir.entries(tmpdir).select{|f| File.file? f}
-    end
-  end
-
-  test "ping new node with no hostname and default config" do
-    node = ping_node(:new_with_no_hostname, {})
-    slot_number = node.slot_number
-    refute_nil slot_number
-    assert_equal("compute#{slot_number}", node.hostname)
-  end
-
-  test "ping new node with no hostname and no config" do
-    Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname = false
-    node = ping_node(:new_with_no_hostname, {})
-    refute_nil node.slot_number
-    assert_nil node.hostname
-  end
-
-  test "ping new node with zero padding config" do
-    Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname = 'compute%<slot_number>04d'
-    node = ping_node(:new_with_no_hostname, {})
-    slot_number = node.slot_number
-    refute_nil slot_number
-    assert_equal("compute000#{slot_number}", node.hostname)
-  end
-
-  test "ping node with hostname and config and expect hostname unchanged" do
-    node = ping_node(:new_with_custom_hostname, {})
-    assert_equal(23, node.slot_number)
-    assert_equal("custom1", node.hostname)
-  end
-
-  test "ping node with hostname and no config and expect hostname unchanged" do
-    Rails.configuration.Containers.SLURM.Managed.AssignNodeHostname = false
-    node = ping_node(:new_with_custom_hostname, {})
-    assert_equal(23, node.slot_number)
-    assert_equal("custom1", node.hostname)
-  end
-
-  # Ping two nodes: one without a hostname and the other with a hostname.
-  # Verify that the first one gets a hostname and second one is unchanged.
-  test "ping two nodes one with no hostname and one with hostname and check hostnames" do
-    # ping node with no hostname and expect it set with config format
-    node = ping_node(:new_with_no_hostname, {})
-    refute_nil node.slot_number
-    assert_equal "compute#{node.slot_number}", node.hostname
-
-    # ping node with a hostname and expect it to be unchanged
-    node2 = ping_node(:new_with_custom_hostname, {})
-    refute_nil node2.slot_number
-    assert_equal "custom1", node2.hostname
-  end
-
-  test "update dns when hostname and ip_address are cleared" do
-    act_as_system_user do
-      node = ping_node(:new_with_custom_hostname, {})
-      Node.expects(:dns_server_update).with(node.hostname, Node::UNUSED_NODE_IP)
-      node.update_attributes(hostname: nil, ip_address: nil)
-    end
-  end
-
-  test "update dns when hostname changes" do
-    act_as_system_user do
-      node = ping_node(:new_with_custom_hostname, {})
-
-      Node.expects(:dns_server_update).with(node.hostname, Node::UNUSED_NODE_IP)
-      Node.expects(:dns_server_update).with('foo0', node.ip_address)
-      node.update_attributes!(hostname: 'foo0')
-
-      Node.expects(:dns_server_update).with('foo0', Node::UNUSED_NODE_IP)
-      node.update_attributes!(hostname: nil, ip_address: nil)
-
-      Node.expects(:dns_server_update).with('foo0', '10.11.12.13')
-      node.update_attributes!(hostname: 'foo0', ip_address: '10.11.12.13')
-
-      Node.expects(:dns_server_update).with('foo0', '10.11.12.14')
-      node.update_attributes!(hostname: 'foo0', ip_address: '10.11.12.14')
-    end
-  end
-
-  test 'newest ping wins IP address conflict' do
-    act_as_system_user do
-      n1, n2 = Node.create!, Node.create!
-
-      n1.ping(ip: '10.5.5.5', ping_secret: n1.info['ping_secret'])
-      n1.reload
-
-      Node.expects(:dns_server_update).with(n1.hostname, Node::UNUSED_NODE_IP)
-      Node.expects(:dns_server_update).with(Not(equals(n1.hostname)), '10.5.5.5')
-      n2.ping(ip: '10.5.5.5', ping_secret: n2.info['ping_secret'])
-
-      n1.reload
-      n2.reload
-      assert_nil n1.ip_address
-      assert_equal '10.5.5.5', n2.ip_address
-
-      Node.expects(:dns_server_update).with(n2.hostname, Node::UNUSED_NODE_IP)
-      Node.expects(:dns_server_update).with(n1.hostname, '10.5.5.5')
-      n1.ping(ip: '10.5.5.5', ping_secret: n1.info['ping_secret'])
-
-      n1.reload
-      n2.reload
-      assert_nil n2.ip_address
-      assert_equal '10.5.5.5', n1.ip_address
-    end
-  end
-
-  test 'run out of slots' do
-    Rails.configuration.Containers.MaxComputeVMs = 3
-    act_as_system_user do
-      Node.destroy_all
-      (1..4).each do |i|
-        n = Node.create!
-        args = { ip: "10.0.0.#{i}", ping_secret: n.info['ping_secret'] }
-        if i <= Rails.configuration.Containers.MaxComputeVMs
-          n.ping(args)
-        else
-          assert_raises do
-            n.ping(args)
-          end
-        end
-      end
-    end
-  end
-end
index 14dd7f3f85c229dd8c60b63766415957204ca850..31afcda8d12267970631372014706793ef95c9f3 100644 (file)
@@ -329,6 +329,8 @@ def catch_exceptions(orig_func):
             raise
         except EnvironmentError as e:
             raise llfuse.FUSEError(e.errno)
+        except NotImplementedError:
+            raise llfuse.FUSEError(errno.ENOTSUP)
         except arvados.errors.KeepWriteError as e:
             _logger.error("Keep write error: " + str(e))
             raise llfuse.FUSEError(errno.EIO)
index a155acd1484b1aa94e2ee75556dba3789bb47619..f4e5138e2ce0fd5d1559046754f2c50a4f1c2ddb 100644 (file)
@@ -6,7 +6,9 @@ from __future__ import absolute_import
 from future.utils import viewitems
 from builtins import str
 from builtins import object
+from pathlib import Path
 from six import assertRegex
+import errno
 import json
 import llfuse
 import logging
@@ -20,6 +22,7 @@ import parameterized
 
 import arvados
 import arvados_fuse as fuse
+from arvados_fuse import fusedir
 from . import run_test_server
 
 from .integration_test import IntegrationTest
@@ -1331,3 +1334,106 @@ class ReadonlyCollectionTest(MountTestBase):
         self.make_mount(fuse.CollectionDirectory, collection_record=self.testcollection, enable_write=False)
 
         self.pool.apply(_readonlyCollectionTestHelper, (self.mounttmp,))
+
+
+@parameterized.parameterized_class([
+    {'root_class': fusedir.ProjectDirectory, 'root_kwargs': {
+        'project_object': run_test_server.fixture('users')['admin'],
+    }},
+    {'root_class': fusedir.ProjectDirectory, 'root_kwargs': {
+        'project_object': run_test_server.fixture('groups')['public'],
+    }},
+])
+class UnsupportedCreateTest(MountTestBase):
+    root_class = None
+    root_kwargs = {}
+
+    def setUp(self):
+        super().setUp()
+        if 'prefs' in self.root_kwargs.get('project_object', ()):
+            self.root_kwargs['project_object']['prefs'] = {}
+        self.make_mount(self.root_class, **self.root_kwargs)
+        # Make sure the directory knows about its top-level ents.
+        os.listdir(self.mounttmp)
+
+    def test_create(self):
+        test_path = Path(self.mounttmp, 'test_create')
+        with self.assertRaises(OSError) as exc_check:
+            with test_path.open('w'):
+                pass
+        self.assertEqual(exc_check.exception.errno, errno.ENOTSUP)
+
+
+# FIXME: IMO, for consistency with the "create inside a project" case,
+# these operations should also return ENOTSUP instead of EPERM.
+# Right now they're returning EPERM because the clasess' writable() method
+# usually returns False, and the Operations class transforms that accordingly.
+# However, for cases where the mount will never be writable, I think ENOTSUP
+# is a clearer error: it lets the user know they can't fix the problem by
+# adding permissions in Arvados, etc.
+@parameterized.parameterized_class([
+    {'root_class': fusedir.MagicDirectory,
+     'preset_dir': 'by_id',
+     'preset_file': 'README',
+     },
+
+    {'root_class': fusedir.SharedDirectory,
+     'root_kwargs': {
+         'exclude': run_test_server.fixture('users')['admin']['uuid'],
+     },
+     'preset_dir': 'Active User',
+     },
+
+    {'root_class': fusedir.TagDirectory,
+     'root_kwargs': {
+         'tag': run_test_server.fixture('links')['foo_collection_tag']['name'],
+     },
+     'preset_dir': run_test_server.fixture('collections')['foo_collection_in_aproject']['uuid'],
+     },
+
+    {'root_class': fusedir.TagsDirectory,
+     'preset_dir': run_test_server.fixture('links')['foo_collection_tag']['name'],
+     },
+])
+class UnsupportedOperationsTest(UnsupportedCreateTest):
+    preset_dir = None
+    preset_file = None
+
+    def test_create(self):
+        test_path = Path(self.mounttmp, 'test_create')
+        with self.assertRaises(OSError) as exc_check:
+            with test_path.open('w'):
+                pass
+        self.assertEqual(exc_check.exception.errno, errno.EPERM)
+
+    def test_mkdir(self):
+        test_path = Path(self.mounttmp, 'test_mkdir')
+        with self.assertRaises(OSError) as exc_check:
+            test_path.mkdir()
+        self.assertEqual(exc_check.exception.errno, errno.EPERM)
+
+    def test_rename(self):
+        src_name = self.preset_dir or self.preset_file
+        if src_name is None:
+            return
+        test_src = Path(self.mounttmp, src_name)
+        test_dst = test_src.with_name('test_dst')
+        with self.assertRaises(OSError) as exc_check:
+            test_src.rename(test_dst)
+        self.assertEqual(exc_check.exception.errno, errno.EPERM)
+
+    def test_rmdir(self):
+        if self.preset_dir is None:
+            return
+        test_path = Path(self.mounttmp, self.preset_dir)
+        with self.assertRaises(OSError) as exc_check:
+            test_path.rmdir()
+        self.assertEqual(exc_check.exception.errno, errno.EPERM)
+
+    def test_unlink(self):
+        if self.preset_file is None:
+            return
+        test_path = Path(self.mounttmp, self.preset_file)
+        with self.assertRaises(OSError) as exc_check:
+            test_path.unlink()
+        self.assertEqual(exc_check.exception.errno, errno.EPERM)