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