1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.util.statsd;
17 
18 import com.android.internal.os.StatsdConfigProto.AtomMatcher;
19 import com.android.internal.os.StatsdConfigProto.EventMetric;
20 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
21 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.ITestDevice;
24 import com.android.tradefed.log.LogUtil.CLog;
25 import com.android.tradefed.util.CommandResult;
26 import com.android.tradefed.util.FileUtil;
27 
28 import com.google.common.io.Files;
29 
30 import java.io.File;
31 import java.io.FileNotFoundException;
32 import java.io.IOException;
33 import java.util.Arrays;
34 import java.util.List;
35 import java.util.UUID;
36 import java.util.stream.Collectors;
37 
38 /**
39  * Utility class for creating, interacting with, and pushing statsd configuration files.
40  *
41  * <p>TODO(b/118635164): Merge with device-side configuration utilities.
42  */
43 public class ConfigUtil {
44     private static final String REMOVE_CONFIG_CMD = "cmd stats config remove";
45     private static final String UPDATE_CONFIG_CMD = "cmd stats config update";
46 
47     /**
48      * Pushes an event-based configuration file to collect atoms provided in {@code eventAtomIds}.
49      *
50      * @param device where to push the configuration
51      * @param eventAtomIds a list of event atom IDs to collect
52      * @return ID of the newly pushed configuration file
53      */
pushStatsConfig(ITestDevice device, List<Integer> eventAtomIds)54     public static long pushStatsConfig(ITestDevice device, List<Integer> eventAtomIds)
55             throws IOException, DeviceNotAvailableException {
56         StatsdConfig config = generateStatsdConfig(eventAtomIds);
57         CLog.d(
58                 "Collecting atoms [%s] with the following config: %s",
59                 eventAtomIds.stream().map(String::valueOf).collect(Collectors.joining(", ")),
60                 config.toString());
61         File configFile = null;
62         try {
63             configFile = File.createTempFile("statsdconfig", ".config");
64             Files.write(config.toByteArray(), configFile);
65             String remotePath = String.format("/data/local/tmp/%s", configFile.getName());
66             if (device.pushFile(configFile, remotePath)) {
67                 updateConfig(device, remotePath, config.getId());
68             } else {
69                 throw new RuntimeException("Failed to configuration push file to the device.");
70             }
71             return config.getId();
72         } finally {
73             FileUtil.deleteFile(configFile);
74         }
75     }
76 
77     /**
78      * Pushes a binary statsd configuration file to collect metrics
79      *
80      * @param device Test device where the binary statsd config will be pushed to
81      * @param configFile The statsd config file
82      * @return ID of the newly pushed configuration file
83      */
pushBinaryStatsConfig(ITestDevice device, File configFile)84     public static long pushBinaryStatsConfig(ITestDevice device, File configFile)
85             throws IOException, DeviceNotAvailableException {
86         if (!configFile.exists()) {
87             throw new FileNotFoundException(
88                     String.format(
89                             "File not found for statsd config: %s", configFile.getAbsolutePath()));
90         }
91         String remotePath = String.format("/data/local/tmp/%s", configFile.getName());
92         if (device.pushFile(configFile, remotePath)) {
93             // For now use a fixed config Id. We will parse it from config file when necessary.
94             long configId = UUID.randomUUID().hashCode();
95             updateConfig(device, remotePath, configId);
96             return configId;
97         } else {
98             throw new RuntimeException("Failed to push configuration file to the device.");
99         }
100     }
101 
102     /**
103      * Removes a statsd configuration file by it's id, {@code configId}.
104      *
105      * @param device where to delete the configuration
106      * @param configId ID of the configuration to delete
107      */
removeConfig(ITestDevice device, long configId)108     public static void removeConfig(ITestDevice device, long configId)
109             throws DeviceNotAvailableException {
110         device.executeShellCommand(String.join(" ", REMOVE_CONFIG_CMD, String.valueOf(configId)));
111     }
112 
113     /**
114      * Push a statsd configuration from a config file on device.
115      *
116      * @param device Test device where the config will be uploaded
117      * @param remotePath The path to the statsd config on the device
118      * @param configId ID of the statsd config
119      */
updateConfig(ITestDevice device, String remotePath, long configId)120     private static void updateConfig(ITestDevice device, String remotePath, long configId)
121             throws DeviceNotAvailableException {
122         CommandResult output =
123                 device.executeShellV2Command(
124                         String.join(
125                                 " ",
126                                 "cat",
127                                 remotePath,
128                                 "|",
129                                 UPDATE_CONFIG_CMD,
130                                 String.valueOf(configId)));
131         device.deleteFile(remotePath);
132         if (output.getStderr().contains("Error parsing")) {
133             throw new RuntimeException("Failed to parse configuration file on the device.");
134         } else if (!output.getStderr().isEmpty()) {
135             throw new RuntimeException(
136                     String.format("Failed to push config with error: %s.", output.getStderr()));
137         }
138     }
139 
140     /**
141      * Creates an statsd configuration file that will collect the event atoms provided in {@code
142      * eventAtomIds}. Note this only accepts data from the list of {@code #commonLogSources} now.
143      *
144      * @param eventAtomIds a list of event atom IDs to collect
145      * @return the {@code StatsdConfig} device configuration
146      */
generateStatsdConfig(List<Integer> eventAtomIds)147     private static StatsdConfig generateStatsdConfig(List<Integer> eventAtomIds) {
148         long configId = UUID.randomUUID().hashCode();
149         StatsdConfig.Builder configBuilder =
150                 StatsdConfig.newBuilder()
151                         .setId(configId)
152                         .addAllAllowedLogSource(commonLogSources());
153         // Add all event atom matchers.
154         for (Integer id : eventAtomIds) {
155             long atomMatcherId = UUID.randomUUID().hashCode();
156             long eventMatcherId = UUID.randomUUID().hashCode();
157             configBuilder =
158                     configBuilder
159                             .addAtomMatcher(
160                                     AtomMatcher.newBuilder()
161                                             .setId(atomMatcherId)
162                                             .setSimpleAtomMatcher(
163                                                     SimpleAtomMatcher.newBuilder().setAtomId(id)))
164                             .addEventMetric(
165                                     EventMetric.newBuilder()
166                                             .setId(eventMatcherId)
167                                             .setWhat(atomMatcherId));
168         }
169         return configBuilder.build();
170     }
171 
172     /** Returns a list of common trusted log sources. */
commonLogSources()173     private static List<String> commonLogSources() {
174         return Arrays.asList(
175                 "AID_BLUETOOTH",
176                 "AID_GRAPHICS",
177                 "AID_INCIENTD",
178                 "AID_RADIO",
179                 "AID_ROOT",
180                 "AID_STATSD",
181                 "AID_SYSTEM");
182     }
183 }
184