9fee66e1ddcb3f96463fe240a11f905be46db0cc
[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) TestTimestampRange(c *check.C) {
162         var stdout, stderr bytes.Buffer
163         resultsDir := c.MkDir()
164         // Run costanalyzer with a timestamp range. This should pick up two container requests in "Final" state.
165         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, "-begin", "2020-11-02T00:00:00", "-end", "2020-11-03T23:59:00"}, &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.CompletedDiagnosticsContainerRequest1UUID + ".csv")
170         c.Assert(err, check.IsNil)
171         uuid2Report, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
172         c.Assert(err, check.IsNil)
173
174         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,763.467,,,,0.01")
175         c.Check(string(uuid2Report), check.Matches, "(?ms).*TOTAL,,,,,,488.775,,,,0.01")
176         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
177         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
178
179         aggregateCostReport, err := ioutil.ReadFile(matches[1])
180         c.Assert(err, check.IsNil)
181
182         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,1245.564,0.01")
183 }
184
185 func (*Suite) TestContainerRequestUUID(c *check.C) {
186         var stdout, stderr bytes.Buffer
187         resultsDir := c.MkDir()
188         // Run costanalyzer with 1 container request uuid
189         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID}, &bytes.Buffer{}, &stdout, &stderr)
190         c.Check(exitcode, check.Equals, 0)
191         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
192
193         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
194         c.Assert(err, check.IsNil)
195         // Make sure the 'preemptible' flag was picked up
196         c.Check(string(uuidReport), check.Matches, "(?ms).*,Standard_E4s_v3,true,.*")
197         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
198         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
199         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
200
201         aggregateCostReport, err := ioutil.ReadFile(matches[1])
202         c.Assert(err, check.IsNil)
203
204         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,86462.000,7.01")
205 }
206
207 func (*Suite) TestCollectionUUID(c *check.C) {
208         var stdout, stderr bytes.Buffer
209
210         resultsDir := c.MkDir()
211         // Run costanalyzer with 1 collection uuid, without 'container_request' property
212         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
213         c.Check(exitcode, check.Equals, 2)
214         c.Assert(stderr.String(), check.Matches, "(?ms).*does not have a 'container_request' property.*")
215
216         // Update the collection, attach a 'container_request' property
217         ac := arvados.NewClientFromEnv()
218         var coll arvados.Collection
219
220         // Update collection record
221         err := ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+arvadostest.FooCollection, nil, map[string]interface{}{
222                 "collection": map[string]interface{}{
223                         "properties": map[string]interface{}{
224                                 "container_request": arvadostest.CompletedContainerRequestUUID,
225                         },
226                 },
227         })
228         c.Assert(err, check.IsNil)
229
230         stdout.Truncate(0)
231         stderr.Truncate(0)
232
233         // Run costanalyzer with 1 collection uuid
234         resultsDir = c.MkDir()
235         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
236         c.Check(exitcode, check.Equals, 0)
237         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
238
239         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
240         c.Assert(err, check.IsNil)
241         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
242         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
243         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
244
245         aggregateCostReport, err := ioutil.ReadFile(matches[1])
246         c.Assert(err, check.IsNil)
247
248         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,86462.000,7.01")
249 }
250
251 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
252         var stdout, stderr bytes.Buffer
253         resultsDir := c.MkDir()
254         // Run costanalyzer with 2 container request uuids
255         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID, arvadostest.CompletedContainerRequestUUID2}, &bytes.Buffer{}, &stdout, &stderr)
256         c.Check(exitcode, check.Equals, 0)
257         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
258
259         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
260         c.Assert(err, check.IsNil)
261         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
262
263         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
264         c.Assert(err, check.IsNil)
265         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,42.27")
266
267         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
268         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
269
270         aggregateCostReport, err := ioutil.ReadFile(matches[1])
271         c.Assert(err, check.IsNil)
272
273         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,172924.000,49.28")
274         stdout.Truncate(0)
275         stderr.Truncate(0)
276
277         // Now move both container requests into an existing project, and then re-run
278         // the analysis with the project uuid. The results should be identical.
279         ac := arvados.NewClientFromEnv()
280         var cr arvados.ContainerRequest
281         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
282                 "container_request": map[string]interface{}{
283                         "owner_uuid": arvadostest.AProjectUUID,
284                 },
285         })
286         c.Assert(err, check.IsNil)
287         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
288                 "container_request": map[string]interface{}{
289                         "owner_uuid": arvadostest.AProjectUUID,
290                 },
291         })
292         c.Assert(err, check.IsNil)
293
294         // Run costanalyzer with the project uuid
295         resultsDir = c.MkDir()
296         exitcode = Command.RunCommand("costanalyzer.test", []string{"-cache=false", "-log-level", "debug", "-output", resultsDir, arvadostest.AProjectUUID}, &bytes.Buffer{}, &stdout, &stderr)
297         c.Check(exitcode, check.Equals, 0)
298         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
299
300         uuidReport, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
301         c.Assert(err, check.IsNil)
302         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
303
304         uuidReport2, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
305         c.Assert(err, check.IsNil)
306         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,42.27")
307
308         re = regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
309         matches = re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
310
311         aggregateCostReport, err = ioutil.ReadFile(matches[1])
312         c.Assert(err, check.IsNil)
313
314         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,172924.000,49.28")
315 }
316
317 func (*Suite) TestUncommittedContainerRequest(c *check.C) {
318         var stdout, stderr bytes.Buffer
319         // Run costanalyzer with 2 container request uuids, one of which is in the Uncommitted state, without output directory specified
320         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.UncommittedContainerRequestUUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
321         c.Check(exitcode, check.Equals, 0)
322         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
323         c.Assert(stderr.String(), check.Matches, "(?ms).*No container associated with container request .*")
324
325         // Check that the total amount was printed to stdout
326         c.Check(stdout.String(), check.Matches, "0.01\n")
327 }
328
329 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
330         var stdout, stderr bytes.Buffer
331         // Run costanalyzer with 2 container request uuids, without output directory specified
332         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
333         c.Check(exitcode, check.Equals, 0)
334         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
335
336         // Check that the total amount was printed to stdout
337         c.Check(stdout.String(), check.Matches, "0.01\n")
338
339         stdout.Truncate(0)
340         stderr.Truncate(0)
341
342         // Run costanalyzer with 2 container request uuids
343         resultsDir := c.MkDir()
344         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
345         c.Check(exitcode, check.Equals, 0)
346         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
347
348         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
349         c.Assert(err, check.IsNil)
350         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,763.467,,,,0.01")
351
352         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
353         c.Assert(err, check.IsNil)
354         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,488.775,,,,0.01")
355
356         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
357         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
358
359         aggregateCostReport, err := ioutil.ReadFile(matches[1])
360         c.Assert(err, check.IsNil)
361
362         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,1245.564,0.01")
363 }