Merge branch '18874-merge-wb2'
[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.StartKeep(2, true)
37
38         // Use a small page size to exercise paging without adding
39         // lots of fixtures
40         pagesize = 2
41
42         // Get the various arvados, arvadosclient, and keep client objects
43         ac := arvados.NewClientFromEnv()
44         arv, err := arvadosclient.MakeArvadosClient()
45         c.Assert(err, check.Equals, nil)
46         arv.ApiToken = arvadostest.ActiveToken
47         kc, err := keepclient.MakeKeepClient(arv)
48         c.Assert(err, check.Equals, nil)
49
50         standardE4sV3JSON := `{
51     "Name": "Standard_E4s_v3",
52     "ProviderType": "Standard_E4s_v3",
53     "VCPUs": 4,
54     "RAM": 34359738368,
55     "Scratch": 64000000000,
56     "IncludedScratch": 64000000000,
57     "AddedScratch": 0,
58     "Price": 0.292,
59     "Preemptible": true
60 }`
61         standardD32sV3JSON := `{
62     "Name": "Standard_D32s_v3",
63     "ProviderType": "Standard_D32s_v3",
64     "VCPUs": 32,
65     "RAM": 137438953472,
66     "Scratch": 256000000000,
67     "IncludedScratch": 256000000000,
68     "AddedScratch": 0,
69     "Price": 1.76,
70     "Preemptible": false
71 }`
72
73         standardA1V2JSON := `{
74     "Name": "a1v2",
75     "ProviderType": "Standard_A1_v2",
76     "VCPUs": 1,
77     "RAM": 2147483648,
78     "Scratch": 10000000000,
79     "IncludedScratch": 10000000000,
80     "AddedScratch": 0,
81     "Price": 0.043,
82     "Preemptible": false
83 }`
84
85         standardA2V2JSON := `{
86     "Name": "a2v2",
87     "ProviderType": "Standard_A2_v2",
88     "VCPUs": 2,
89     "RAM": 4294967296,
90     "Scratch": 20000000000,
91     "IncludedScratch": 20000000000,
92     "AddedScratch": 0,
93     "Price": 0.091,
94     "Preemptible": false
95 }`
96
97         legacyD1V2JSON := `{
98     "properties": {
99         "cloud_node": {
100             "price": 0.073001,
101             "size": "Standard_D1_v2"
102         },
103         "total_cpu_cores": 1,
104         "total_ram_mb": 3418,
105         "total_scratch_mb": 51170
106     }
107 }`
108
109         // Our fixtures do not actually contain file contents. Populate the log collections we're going to use with the node.json file
110         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID, arvadostest.LogCollectionUUID, standardE4sV3JSON)
111         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID2, arvadostest.LogCollectionUUID2, standardD32sV3JSON)
112
113         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.DiagnosticsContainerRequest1LogCollectionUUID, standardA1V2JSON)
114         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest2UUID, arvadostest.DiagnosticsContainerRequest2LogCollectionUUID, standardA1V2JSON)
115         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher1ContainerRequestUUID, arvadostest.Hasher1LogCollectionUUID, standardA1V2JSON)
116         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher2ContainerRequestUUID, arvadostest.Hasher2LogCollectionUUID, standardA2V2JSON)
117         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher3ContainerRequestUUID, arvadostest.Hasher3LogCollectionUUID, legacyD1V2JSON)
118 }
119
120 func createNodeJSON(c *check.C, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, crUUID string, logUUID string, nodeJSON string) {
121         // Get the CR
122         var cr arvados.ContainerRequest
123         err := ac.RequestAndDecode(&cr, "GET", "arvados/v1/container_requests/"+crUUID, nil, nil)
124         c.Assert(err, check.Equals, nil)
125         c.Assert(cr.LogUUID, check.Equals, logUUID)
126
127         // Get the log collection
128         var coll arvados.Collection
129         err = ac.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+cr.LogUUID, nil, nil)
130         c.Assert(err, check.IsNil)
131
132         // Create a node.json file -- the fixture doesn't actually contain the contents of the collection.
133         fs, err := coll.FileSystem(ac, kc)
134         c.Assert(err, check.IsNil)
135         f, err := fs.OpenFile("node.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
136         c.Assert(err, check.IsNil)
137         _, err = io.WriteString(f, nodeJSON)
138         c.Assert(err, check.IsNil)
139         err = f.Close()
140         c.Assert(err, check.IsNil)
141
142         // Flush the data to Keep
143         mtxt, err := fs.MarshalManifest(".")
144         c.Assert(err, check.IsNil)
145         c.Assert(mtxt, check.NotNil)
146
147         // Update collection record
148         err = ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+cr.LogUUID, nil, map[string]interface{}{
149                 "collection": map[string]interface{}{
150                         "manifest_text": mtxt,
151                 },
152         })
153         c.Assert(err, check.IsNil)
154 }
155
156 func (*Suite) TestUsage(c *check.C) {
157         var stdout, stderr bytes.Buffer
158         exitcode := Command.RunCommand("costanalyzer.test", []string{"-help", "-log-level=debug"}, &bytes.Buffer{}, &stdout, &stderr)
159         c.Check(exitcode, check.Equals, 0)
160         c.Check(stdout.String(), check.Equals, "")
161         c.Check(stderr.String(), check.Matches, `(?ms).*Usage:.*`)
162 }
163
164 func (*Suite) TestTimestampRange(c *check.C) {
165         var stdout, stderr bytes.Buffer
166         resultsDir := c.MkDir()
167         // Run costanalyzer with a timestamp range. This should pick up two container requests in "Final" state.
168         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, "-begin", "2020-11-02T00:00:00", "-end", "2020-11-03T23:59:00"}, &bytes.Buffer{}, &stdout, &stderr)
169         c.Check(exitcode, check.Equals, 0)
170         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
171
172         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
173         c.Assert(err, check.IsNil)
174         uuid2Report, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
175         c.Assert(err, check.IsNil)
176
177         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,763.467,,,,0.01")
178         c.Check(string(uuid2Report), check.Matches, "(?ms).*TOTAL,,,,,,488.775,,,,0.01")
179         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
180         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
181
182         aggregateCostReport, err := ioutil.ReadFile(matches[1])
183         c.Assert(err, check.IsNil)
184
185         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,1245.564,0.01")
186 }
187
188 func (*Suite) TestContainerRequestUUID(c *check.C) {
189         var stdout, stderr bytes.Buffer
190         resultsDir := c.MkDir()
191         // Run costanalyzer with 1 container request uuid
192         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID}, &bytes.Buffer{}, &stdout, &stderr)
193         c.Check(exitcode, check.Equals, 0)
194         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
195
196         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
197         c.Assert(err, check.IsNil)
198         // Make sure the 'preemptible' flag was picked up
199         c.Check(string(uuidReport), check.Matches, "(?ms).*,Standard_E4s_v3,true,.*")
200         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
201         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
202         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
203
204         aggregateCostReport, err := ioutil.ReadFile(matches[1])
205         c.Assert(err, check.IsNil)
206
207         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,86462.000,7.01")
208 }
209
210 func (*Suite) TestCollectionUUID(c *check.C) {
211         var stdout, stderr bytes.Buffer
212         resultsDir := c.MkDir()
213
214         // Create a collection with no container_request property
215         ac := arvados.NewClientFromEnv()
216         var coll arvados.Collection
217         err := ac.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, nil)
218         c.Assert(err, check.IsNil)
219
220         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, coll.UUID}, &bytes.Buffer{}, &stdout, &stderr)
221         c.Check(exitcode, check.Equals, 2)
222         c.Assert(stderr.String(), check.Matches, "(?ms).*does not have a 'container_request' property.*")
223
224         stdout.Truncate(0)
225         stderr.Truncate(0)
226
227         // Add a container_request property
228         err = ac.RequestAndDecode(&coll, "PATCH", "arvados/v1/collections/"+coll.UUID, nil, map[string]interface{}{
229                 "collection": map[string]interface{}{
230                         "properties": map[string]interface{}{
231                                 "container_request": arvadostest.CompletedContainerRequestUUID,
232                         },
233                 },
234         })
235         c.Assert(err, check.IsNil)
236
237         // Re-run costanalyzer on the updated collection
238         resultsDir = c.MkDir()
239         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, coll.UUID}, &bytes.Buffer{}, &stdout, &stderr)
240         c.Check(exitcode, check.Equals, 0)
241         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
242
243         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
244         c.Assert(err, check.IsNil)
245         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
246         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
247         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
248
249         aggregateCostReport, err := ioutil.ReadFile(matches[1])
250         c.Assert(err, check.IsNil)
251
252         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,86462.000,7.01")
253 }
254
255 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
256         var stdout, stderr bytes.Buffer
257         resultsDir := c.MkDir()
258         // Run costanalyzer with 2 container request uuids
259         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID, arvadostest.CompletedContainerRequestUUID2}, &bytes.Buffer{}, &stdout, &stderr)
260         c.Check(exitcode, check.Equals, 0)
261         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
262
263         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
264         c.Assert(err, check.IsNil)
265         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
266
267         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
268         c.Assert(err, check.IsNil)
269         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,42.27")
270
271         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
272         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
273
274         aggregateCostReport, err := ioutil.ReadFile(matches[1])
275         c.Assert(err, check.IsNil)
276
277         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,172924.000,49.28")
278         stdout.Truncate(0)
279         stderr.Truncate(0)
280
281         // Now move both container requests into an existing project, and then re-run
282         // the analysis with the project uuid. The results should be identical.
283         ac := arvados.NewClientFromEnv()
284         var cr arvados.ContainerRequest
285         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
286                 "container_request": map[string]interface{}{
287                         "owner_uuid": arvadostest.AProjectUUID,
288                 },
289         })
290         c.Assert(err, check.IsNil)
291         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
292                 "container_request": map[string]interface{}{
293                         "owner_uuid": arvadostest.AProjectUUID,
294                 },
295         })
296         c.Assert(err, check.IsNil)
297
298         // Run costanalyzer with the project uuid
299         resultsDir = c.MkDir()
300         exitcode = Command.RunCommand("costanalyzer.test", []string{"-cache=false", "-log-level", "debug", "-output", resultsDir, arvadostest.AProjectUUID}, &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(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
305         c.Assert(err, check.IsNil)
306         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
307
308         uuidReport2, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
309         c.Assert(err, check.IsNil)
310         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,42.27")
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,172924.000,49.28")
319 }
320
321 func (*Suite) TestUncommittedContainerRequest(c *check.C) {
322         var stdout, stderr bytes.Buffer
323         // Run costanalyzer with 2 container request uuids, one of which is in the Uncommitted state, without output directory specified
324         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.UncommittedContainerRequestUUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
325         c.Check(exitcode, check.Equals, 0)
326         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
327         c.Assert(stderr.String(), check.Matches, "(?ms).*No container associated with container request .*")
328
329         // Check that the total amount was printed to stdout
330         c.Check(stdout.String(), check.Matches, "0.01\n")
331 }
332
333 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
334         var stdout, stderr bytes.Buffer
335         // Run costanalyzer with 2 container request uuids, without output directory specified
336         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
337         c.Check(exitcode, check.Equals, 0)
338         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
339
340         // Check that the total amount was printed to stdout
341         c.Check(stdout.String(), check.Matches, "0.01\n")
342
343         stdout.Truncate(0)
344         stderr.Truncate(0)
345
346         // Run costanalyzer with 2 container request uuids
347         resultsDir := c.MkDir()
348         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
349         c.Check(exitcode, check.Equals, 0)
350         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
351
352         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
353         c.Assert(err, check.IsNil)
354         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,763.467,,,,0.01")
355
356         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
357         c.Assert(err, check.IsNil)
358         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,488.775,,,,0.01")
359
360         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
361         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
362
363         aggregateCostReport, err := ioutil.ReadFile(matches[1])
364         c.Assert(err, check.IsNil)
365
366         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,1245.564,0.01")
367 }