16306: Merge branch 'master'
[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": false
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         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
172         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
173         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
174
175         aggregateCostReport, err := ioutil.ReadFile(matches[1])
176         c.Assert(err, check.IsNil)
177
178         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
179 }
180
181 func (*Suite) TestCollectionUUID(c *check.C) {
182         var stdout, stderr bytes.Buffer
183
184         resultsDir := c.MkDir()
185         // Run costanalyzer with 1 collection uuid, without 'container_request' property
186         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
187         c.Check(exitcode, check.Equals, 2)
188         c.Assert(stderr.String(), check.Matches, "(?ms).*does not have a 'container_request' property.*")
189
190         // Update the collection, attach a 'container_request' property
191         ac := arvados.NewClientFromEnv()
192         var coll arvados.Collection
193
194         // Update collection record
195         err := ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+arvadostest.FooCollection, nil, map[string]interface{}{
196                 "collection": map[string]interface{}{
197                         "properties": map[string]interface{}{
198                                 "container_request": arvadostest.CompletedContainerRequestUUID,
199                         },
200                 },
201         })
202         c.Assert(err, check.IsNil)
203
204         stdout.Truncate(0)
205         stderr.Truncate(0)
206
207         // Run costanalyzer with 1 collection uuid
208         resultsDir = c.MkDir()
209         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
210         c.Check(exitcode, check.Equals, 0)
211         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
212
213         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
214         c.Assert(err, check.IsNil)
215         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
216         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
217         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
218
219         aggregateCostReport, err := ioutil.ReadFile(matches[1])
220         c.Assert(err, check.IsNil)
221
222         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
223 }
224
225 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
226         var stdout, stderr bytes.Buffer
227         resultsDir := c.MkDir()
228         // Run costanalyzer with 2 container request uuids
229         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID, arvadostest.CompletedContainerRequestUUID2}, &bytes.Buffer{}, &stdout, &stderr)
230         c.Check(exitcode, check.Equals, 0)
231         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
232
233         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
234         c.Assert(err, check.IsNil)
235         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
236
237         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
238         c.Assert(err, check.IsNil)
239         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
240
241         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
242         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
243
244         aggregateCostReport, err := ioutil.ReadFile(matches[1])
245         c.Assert(err, check.IsNil)
246
247         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
248         stdout.Truncate(0)
249         stderr.Truncate(0)
250
251         // Now move both container requests into an existing project, and then re-run
252         // the analysis with the project uuid. The results should be identical.
253         ac := arvados.NewClientFromEnv()
254         var cr arvados.ContainerRequest
255         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
256                 "container_request": map[string]interface{}{
257                         "owner_uuid": arvadostest.AProjectUUID,
258                 },
259         })
260         c.Assert(err, check.IsNil)
261         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
262                 "container_request": map[string]interface{}{
263                         "owner_uuid": arvadostest.AProjectUUID,
264                 },
265         })
266         c.Assert(err, check.IsNil)
267
268         // Run costanalyzer with the project uuid
269         resultsDir = c.MkDir()
270         exitcode = Command.RunCommand("costanalyzer.test", []string{"-cache=false", "-log-level", "debug", "-output", resultsDir, arvadostest.AProjectUUID}, &bytes.Buffer{}, &stdout, &stderr)
271         c.Check(exitcode, check.Equals, 0)
272         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
273
274         uuidReport, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
275         c.Assert(err, check.IsNil)
276         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
277
278         uuidReport2, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
279         c.Assert(err, check.IsNil)
280         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
281
282         re = regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
283         matches = re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
284
285         aggregateCostReport, err = ioutil.ReadFile(matches[1])
286         c.Assert(err, check.IsNil)
287
288         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
289 }
290
291 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
292         var stdout, stderr bytes.Buffer
293         // Run costanalyzer with 2 container request uuids, without output directory specified
294         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
295         c.Check(exitcode, check.Equals, 0)
296         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
297
298         // Check that the total amount was printed to stdout
299         c.Check(stdout.String(), check.Matches, "0.01492030\n")
300
301         stdout.Truncate(0)
302         stderr.Truncate(0)
303
304         // Run costanalyzer with 2 container request uuids
305         resultsDir := c.MkDir()
306         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
307         c.Check(exitcode, check.Equals, 0)
308         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
309
310         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
311         c.Assert(err, check.IsNil)
312         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00916192")
313
314         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
315         c.Assert(err, check.IsNil)
316         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00588088")
317
318         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
319         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
320
321         aggregateCostReport, err := ioutil.ReadFile(matches[1])
322         c.Assert(err, check.IsNil)
323
324         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,0.01492030")
325 }