18992: Fix broken-node-hook global flag.
[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         // Get the various arvados, arvadosclient, and keep client objects
39         ac := arvados.NewClientFromEnv()
40         arv, err := arvadosclient.MakeArvadosClient()
41         c.Assert(err, check.Equals, nil)
42         arv.ApiToken = arvadostest.ActiveToken
43         kc, err := keepclient.MakeKeepClient(arv)
44         c.Assert(err, check.Equals, nil)
45
46         standardE4sV3JSON := `{
47     "Name": "Standard_E4s_v3",
48     "ProviderType": "Standard_E4s_v3",
49     "VCPUs": 4,
50     "RAM": 34359738368,
51     "Scratch": 64000000000,
52     "IncludedScratch": 64000000000,
53     "AddedScratch": 0,
54     "Price": 0.292,
55     "Preemptible": true
56 }`
57         standardD32sV3JSON := `{
58     "Name": "Standard_D32s_v3",
59     "ProviderType": "Standard_D32s_v3",
60     "VCPUs": 32,
61     "RAM": 137438953472,
62     "Scratch": 256000000000,
63     "IncludedScratch": 256000000000,
64     "AddedScratch": 0,
65     "Price": 1.76,
66     "Preemptible": false
67 }`
68
69         standardA1V2JSON := `{
70     "Name": "a1v2",
71     "ProviderType": "Standard_A1_v2",
72     "VCPUs": 1,
73     "RAM": 2147483648,
74     "Scratch": 10000000000,
75     "IncludedScratch": 10000000000,
76     "AddedScratch": 0,
77     "Price": 0.043,
78     "Preemptible": false
79 }`
80
81         standardA2V2JSON := `{
82     "Name": "a2v2",
83     "ProviderType": "Standard_A2_v2",
84     "VCPUs": 2,
85     "RAM": 4294967296,
86     "Scratch": 20000000000,
87     "IncludedScratch": 20000000000,
88     "AddedScratch": 0,
89     "Price": 0.091,
90     "Preemptible": false
91 }`
92
93         legacyD1V2JSON := `{
94     "properties": {
95         "cloud_node": {
96             "price": 0.073001,
97             "size": "Standard_D1_v2"
98         },
99         "total_cpu_cores": 1,
100         "total_ram_mb": 3418,
101         "total_scratch_mb": 51170
102     }
103 }`
104
105         // Our fixtures do not actually contain file contents. Populate the log collections we're going to use with the node.json file
106         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID, arvadostest.LogCollectionUUID, standardE4sV3JSON)
107         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID2, arvadostest.LogCollectionUUID2, standardD32sV3JSON)
108
109         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.DiagnosticsContainerRequest1LogCollectionUUID, standardA1V2JSON)
110         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest2UUID, arvadostest.DiagnosticsContainerRequest2LogCollectionUUID, standardA1V2JSON)
111         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher1ContainerRequestUUID, arvadostest.Hasher1LogCollectionUUID, standardA1V2JSON)
112         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher2ContainerRequestUUID, arvadostest.Hasher2LogCollectionUUID, standardA2V2JSON)
113         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher3ContainerRequestUUID, arvadostest.Hasher3LogCollectionUUID, legacyD1V2JSON)
114 }
115
116 func createNodeJSON(c *check.C, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, crUUID string, logUUID string, nodeJSON string) {
117         // Get the CR
118         var cr arvados.ContainerRequest
119         err := ac.RequestAndDecode(&cr, "GET", "arvados/v1/container_requests/"+crUUID, nil, nil)
120         c.Assert(err, check.Equals, nil)
121         c.Assert(cr.LogUUID, check.Equals, logUUID)
122
123         // Get the log collection
124         var coll arvados.Collection
125         err = ac.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+cr.LogUUID, nil, nil)
126         c.Assert(err, check.IsNil)
127
128         // Create a node.json file -- the fixture doesn't actually contain the contents of the collection.
129         fs, err := coll.FileSystem(ac, kc)
130         c.Assert(err, check.IsNil)
131         f, err := fs.OpenFile("node.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
132         c.Assert(err, check.IsNil)
133         _, err = io.WriteString(f, nodeJSON)
134         c.Assert(err, check.IsNil)
135         err = f.Close()
136         c.Assert(err, check.IsNil)
137
138         // Flush the data to Keep
139         mtxt, err := fs.MarshalManifest(".")
140         c.Assert(err, check.IsNil)
141         c.Assert(mtxt, check.NotNil)
142
143         // Update collection record
144         err = ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+cr.LogUUID, nil, map[string]interface{}{
145                 "collection": map[string]interface{}{
146                         "manifest_text": mtxt,
147                 },
148         })
149         c.Assert(err, check.IsNil)
150 }
151
152 func (*Suite) TestUsage(c *check.C) {
153         var stdout, stderr bytes.Buffer
154         exitcode := Command.RunCommand("costanalyzer.test", []string{"-help", "-log-level=debug"}, &bytes.Buffer{}, &stdout, &stderr)
155         c.Check(exitcode, check.Equals, 0)
156         c.Check(stdout.String(), check.Equals, "")
157         c.Check(stderr.String(), check.Matches, `(?ms).*Usage:.*`)
158 }
159
160 func (*Suite) TestTimestampRange(c *check.C) {
161         var stdout, stderr bytes.Buffer
162         resultsDir := c.MkDir()
163         // Run costanalyzer with a timestamp range. This should pick up two container requests in "Final" state.
164         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, "-begin", "2020-11-02T00:00:00", "-end", "2020-11-03T23:59:00"}, &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(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
169         c.Assert(err, check.IsNil)
170         uuid2Report, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
171         c.Assert(err, check.IsNil)
172
173         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,763.467,,,,0.01")
174         c.Check(string(uuid2Report), check.Matches, "(?ms).*TOTAL,,,,,,488.775,,,,0.01")
175         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
176         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
177
178         aggregateCostReport, err := ioutil.ReadFile(matches[1])
179         c.Assert(err, check.IsNil)
180
181         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,1245.564,0.01")
182 }
183
184 func (*Suite) TestContainerRequestUUID(c *check.C) {
185         var stdout, stderr bytes.Buffer
186         resultsDir := c.MkDir()
187         // Run costanalyzer with 1 container request uuid
188         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID}, &bytes.Buffer{}, &stdout, &stderr)
189         c.Check(exitcode, check.Equals, 0)
190         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
191
192         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
193         c.Assert(err, check.IsNil)
194         // Make sure the 'preemptible' flag was picked up
195         c.Check(string(uuidReport), check.Matches, "(?ms).*,Standard_E4s_v3,true,.*")
196         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,86462.000,,,,7.01")
197         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
198         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
199
200         aggregateCostReport, err := ioutil.ReadFile(matches[1])
201         c.Assert(err, check.IsNil)
202
203         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,86462.000,7.01")
204 }
205
206 func (*Suite) TestCollectionUUID(c *check.C) {
207         var stdout, stderr bytes.Buffer
208         resultsDir := c.MkDir()
209
210         // Create a collection with no container_request property
211         ac := arvados.NewClientFromEnv()
212         var coll arvados.Collection
213         err := ac.RequestAndDecode(&coll, "POST", "arvados/v1/collections", nil, nil)
214         c.Assert(err, check.IsNil)
215
216         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, coll.UUID}, &bytes.Buffer{}, &stdout, &stderr)
217         c.Check(exitcode, check.Equals, 2)
218         c.Assert(stderr.String(), check.Matches, "(?ms).*does not have a 'container_request' property.*")
219
220         stdout.Truncate(0)
221         stderr.Truncate(0)
222
223         // Add a container_request property
224         err = ac.RequestAndDecode(&coll, "PATCH", "arvados/v1/collections/"+coll.UUID, nil, map[string]interface{}{
225                 "collection": map[string]interface{}{
226                         "properties": map[string]interface{}{
227                                 "container_request": arvadostest.CompletedContainerRequestUUID,
228                         },
229                 },
230         })
231         c.Assert(err, check.IsNil)
232
233         // Re-run costanalyzer on the updated collection
234         resultsDir = c.MkDir()
235         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, coll.UUID}, &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 }