17529: Heed MaxCloudOpsPerSecond in Instances() and returned insts.
[arvados.git] / lib / costanalyzer / costanalyzer_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package costanalyzer
6
7 import (
8         "bytes"
9         "io"
10         "io/ioutil"
11         "os"
12         "regexp"
13         "testing"
14
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
17         "git.arvados.org/arvados.git/sdk/go/arvadostest"
18         "git.arvados.org/arvados.git/sdk/go/keepclient"
19         "gopkg.in/check.v1"
20 )
21
22 func Test(t *testing.T) {
23         check.TestingT(t)
24 }
25
26 var _ = check.Suite(&Suite{})
27
28 type Suite struct{}
29
30 func (s *Suite) TearDownSuite(c *check.C) {
31         // Undo any changes/additions to the database so they don't affect subsequent tests.
32         arvadostest.ResetEnv()
33 }
34
35 func (s *Suite) SetUpSuite(c *check.C) {
36         arvadostest.StartAPI()
37         arvadostest.StartKeep(2, true)
38
39         // Get the various arvados, arvadosclient, and keep client objects
40         ac := arvados.NewClientFromEnv()
41         arv, err := arvadosclient.MakeArvadosClient()
42         c.Assert(err, check.Equals, nil)
43         arv.ApiToken = arvadostest.ActiveToken
44         kc, err := keepclient.MakeKeepClient(arv)
45         c.Assert(err, check.Equals, nil)
46
47         standardE4sV3JSON := `{
48     "Name": "Standard_E4s_v3",
49     "ProviderType": "Standard_E4s_v3",
50     "VCPUs": 4,
51     "RAM": 34359738368,
52     "Scratch": 64000000000,
53     "IncludedScratch": 64000000000,
54     "AddedScratch": 0,
55     "Price": 0.292,
56     "Preemptible": true
57 }`
58         standardD32sV3JSON := `{
59     "Name": "Standard_D32s_v3",
60     "ProviderType": "Standard_D32s_v3",
61     "VCPUs": 32,
62     "RAM": 137438953472,
63     "Scratch": 256000000000,
64     "IncludedScratch": 256000000000,
65     "AddedScratch": 0,
66     "Price": 1.76,
67     "Preemptible": false
68 }`
69
70         standardA1V2JSON := `{
71     "Name": "a1v2",
72     "ProviderType": "Standard_A1_v2",
73     "VCPUs": 1,
74     "RAM": 2147483648,
75     "Scratch": 10000000000,
76     "IncludedScratch": 10000000000,
77     "AddedScratch": 0,
78     "Price": 0.043,
79     "Preemptible": false
80 }`
81
82         standardA2V2JSON := `{
83     "Name": "a2v2",
84     "ProviderType": "Standard_A2_v2",
85     "VCPUs": 2,
86     "RAM": 4294967296,
87     "Scratch": 20000000000,
88     "IncludedScratch": 20000000000,
89     "AddedScratch": 0,
90     "Price": 0.091,
91     "Preemptible": false
92 }`
93
94         legacyD1V2JSON := `{
95     "properties": {
96         "cloud_node": {
97             "price": 0.073001,
98             "size": "Standard_D1_v2"
99         },
100         "total_cpu_cores": 1,
101         "total_ram_mb": 3418,
102         "total_scratch_mb": 51170
103     }
104 }`
105
106         // Our fixtures do not actually contain file contents. Populate the log collections we're going to use with the node.json file
107         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID, arvadostest.LogCollectionUUID, standardE4sV3JSON)
108         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID2, arvadostest.LogCollectionUUID2, standardD32sV3JSON)
109
110         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.DiagnosticsContainerRequest1LogCollectionUUID, standardA1V2JSON)
111         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest2UUID, arvadostest.DiagnosticsContainerRequest2LogCollectionUUID, standardA1V2JSON)
112         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher1ContainerRequestUUID, arvadostest.Hasher1LogCollectionUUID, standardA1V2JSON)
113         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher2ContainerRequestUUID, arvadostest.Hasher2LogCollectionUUID, standardA2V2JSON)
114         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher3ContainerRequestUUID, arvadostest.Hasher3LogCollectionUUID, legacyD1V2JSON)
115 }
116
117 func createNodeJSON(c *check.C, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, crUUID string, logUUID string, nodeJSON string) {
118         // Get the CR
119         var cr arvados.ContainerRequest
120         err := ac.RequestAndDecode(&cr, "GET", "arvados/v1/container_requests/"+crUUID, nil, nil)
121         c.Assert(err, check.Equals, nil)
122         c.Assert(cr.LogUUID, check.Equals, logUUID)
123
124         // Get the log collection
125         var coll arvados.Collection
126         err = ac.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+cr.LogUUID, nil, nil)
127         c.Assert(err, check.IsNil)
128
129         // Create a node.json file -- the fixture doesn't actually contain the contents of the collection.
130         fs, err := coll.FileSystem(ac, kc)
131         c.Assert(err, check.IsNil)
132         f, err := fs.OpenFile("node.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
133         c.Assert(err, check.IsNil)
134         _, err = io.WriteString(f, nodeJSON)
135         c.Assert(err, check.IsNil)
136         err = f.Close()
137         c.Assert(err, check.IsNil)
138
139         // Flush the data to Keep
140         mtxt, err := fs.MarshalManifest(".")
141         c.Assert(err, check.IsNil)
142         c.Assert(mtxt, check.NotNil)
143
144         // Update collection record
145         err = ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+cr.LogUUID, nil, map[string]interface{}{
146                 "collection": map[string]interface{}{
147                         "manifest_text": mtxt,
148                 },
149         })
150         c.Assert(err, check.IsNil)
151 }
152
153 func (*Suite) TestUsage(c *check.C) {
154         var stdout, stderr bytes.Buffer
155         exitcode := Command.RunCommand("costanalyzer.test", []string{"-help", "-log-level=debug"}, &bytes.Buffer{}, &stdout, &stderr)
156         c.Check(exitcode, check.Equals, 1)
157         c.Check(stdout.String(), check.Equals, "")
158         c.Check(stderr.String(), check.Matches, `(?ms).*Usage:.*`)
159 }
160
161 func (*Suite) TestContainerRequestUUID(c *check.C) {
162         var stdout, stderr bytes.Buffer
163         resultsDir := c.MkDir()
164         // Run costanalyzer with 1 container request uuid
165         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID}, &bytes.Buffer{}, &stdout, &stderr)
166         c.Check(exitcode, check.Equals, 0)
167         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
168
169         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
170         c.Assert(err, check.IsNil)
171         // Make sure the 'preemptible' flag was picked up
172         c.Check(string(uuidReport), check.Matches, "(?ms).*,Standard_E4s_v3,true,.*")
173         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
174         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
175         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
176
177         aggregateCostReport, err := ioutil.ReadFile(matches[1])
178         c.Assert(err, check.IsNil)
179
180         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
181 }
182
183 func (*Suite) TestCollectionUUID(c *check.C) {
184         var stdout, stderr bytes.Buffer
185
186         resultsDir := c.MkDir()
187         // Run costanalyzer with 1 collection uuid, without 'container_request' property
188         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
189         c.Check(exitcode, check.Equals, 2)
190         c.Assert(stderr.String(), check.Matches, "(?ms).*does not have a 'container_request' property.*")
191
192         // Update the collection, attach a 'container_request' property
193         ac := arvados.NewClientFromEnv()
194         var coll arvados.Collection
195
196         // Update collection record
197         err := ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+arvadostest.FooCollection, nil, map[string]interface{}{
198                 "collection": map[string]interface{}{
199                         "properties": map[string]interface{}{
200                                 "container_request": arvadostest.CompletedContainerRequestUUID,
201                         },
202                 },
203         })
204         c.Assert(err, check.IsNil)
205
206         stdout.Truncate(0)
207         stderr.Truncate(0)
208
209         // Run costanalyzer with 1 collection uuid
210         resultsDir = c.MkDir()
211         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
212         c.Check(exitcode, check.Equals, 0)
213         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
214
215         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
216         c.Assert(err, check.IsNil)
217         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
218         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
219         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
220
221         aggregateCostReport, err := ioutil.ReadFile(matches[1])
222         c.Assert(err, check.IsNil)
223
224         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
225 }
226
227 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
228         var stdout, stderr bytes.Buffer
229         resultsDir := c.MkDir()
230         // Run costanalyzer with 2 container request uuids
231         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID, arvadostest.CompletedContainerRequestUUID2}, &bytes.Buffer{}, &stdout, &stderr)
232         c.Check(exitcode, check.Equals, 0)
233         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
234
235         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
236         c.Assert(err, check.IsNil)
237         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
238
239         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
240         c.Assert(err, check.IsNil)
241         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
242
243         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
244         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
245
246         aggregateCostReport, err := ioutil.ReadFile(matches[1])
247         c.Assert(err, check.IsNil)
248
249         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
250         stdout.Truncate(0)
251         stderr.Truncate(0)
252
253         // Now move both container requests into an existing project, and then re-run
254         // the analysis with the project uuid. The results should be identical.
255         ac := arvados.NewClientFromEnv()
256         var cr arvados.ContainerRequest
257         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
258                 "container_request": map[string]interface{}{
259                         "owner_uuid": arvadostest.AProjectUUID,
260                 },
261         })
262         c.Assert(err, check.IsNil)
263         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
264                 "container_request": map[string]interface{}{
265                         "owner_uuid": arvadostest.AProjectUUID,
266                 },
267         })
268         c.Assert(err, check.IsNil)
269
270         // Run costanalyzer with the project uuid
271         resultsDir = c.MkDir()
272         exitcode = Command.RunCommand("costanalyzer.test", []string{"-cache=false", "-log-level", "debug", "-output", resultsDir, arvadostest.AProjectUUID}, &bytes.Buffer{}, &stdout, &stderr)
273         c.Check(exitcode, check.Equals, 0)
274         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
275
276         uuidReport, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
277         c.Assert(err, check.IsNil)
278         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
279
280         uuidReport2, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
281         c.Assert(err, check.IsNil)
282         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
283
284         re = regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
285         matches = re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
286
287         aggregateCostReport, err = ioutil.ReadFile(matches[1])
288         c.Assert(err, check.IsNil)
289
290         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
291 }
292
293 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
294         var stdout, stderr bytes.Buffer
295         // Run costanalyzer with 2 container request uuids, without output directory specified
296         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
297         c.Check(exitcode, check.Equals, 0)
298         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
299
300         // Check that the total amount was printed to stdout
301         c.Check(stdout.String(), check.Matches, "0.01492030\n")
302
303         stdout.Truncate(0)
304         stderr.Truncate(0)
305
306         // Run costanalyzer with 2 container request uuids
307         resultsDir := c.MkDir()
308         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
309         c.Check(exitcode, check.Equals, 0)
310         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
311
312         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
313         c.Assert(err, check.IsNil)
314         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00916192")
315
316         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
317         c.Assert(err, check.IsNil)
318         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00588088")
319
320         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
321         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
322
323         aggregateCostReport, err := ioutil.ReadFile(matches[1])
324         c.Assert(err, check.IsNil)
325
326         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,0.01492030")
327 }