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
17import os
18import datetime
19import logging
20import stat
21import threading
22import zipfile
23
24from host_controller import common
25from host_controller.command_processor import base_command_processor
26
27_REPACKAGE_ADDITIONAL_FILE_LIST = [
28    "android-vtslab/testcases/DATA/app/WifiUtil/WifiUtil.apk",
29    "android-vtslab/testcases/DATA/vtslab-gcs.json",
30    "android-vtslab/testcases/DATA/xml/media_profiles_vendor.xml",
31    "android-vtslab/testcases/host_controller/build/client_secrets.json",
32    "android-vtslab/testcases/host_controller/build/credentials",
33]
34
35_REPACKAGE_ADDITIONAL_BIN_LIST = [
36    "android-vtslab/bin/adb",
37]
38
39# Path to the version.txt file in the fetched vtslab package zip.
40_VERSION_INFO_FILE_PATH = "android-vtslab/testcases/version.txt"
41
42# List of strings for supported ak versions.
43AK_VERSIONS = ["8.0.0", "8.0.1", "8.1.0", "9", "O", "OMR1", "P", "Q"]
44
45for version in AK_VERSIONS:
46    file_path = "android-vtslab/testcases/DATA/ak/.%s.ak" % version
47    _REPACKAGE_ADDITIONAL_FILE_LIST.append(file_path)
48    file_path += ".pub"
49    _REPACKAGE_ADDITIONAL_FILE_LIST.append(file_path)
50
51
52class CommandRelease(base_command_processor.BaseCommandProcessor):
53    """Command processor for update command.
54
55    Attributes:
56        arg_parser: ConsoleArgumentParser object, argument parser.
57        console: cmd.Cmd console object.
58        command: string, command name which this processor will handle.
59        command_detail: string, detailed explanation for the command.
60        _timers: dict, instances of scheduled threading.Timer.
61                 Uses timestamp("%H:%M") string as a key.
62        _vtslab_package_version: string, version information of the fetched
63                                 vtslab package.
64                                 (<git commit timestamp>:<git commit hash value>)
65    """
66
67    command = "release"
68    command_detail = "Release HC. Used for fetching HC package from PAB and uploading to GCS."
69
70    # @Override
71    def SetUp(self):
72        """Initializes the parser for update command."""
73        self._timers = {}
74        self._vtslab_package_version = ""
75        self.arg_parser.add_argument(
76            "--schedule-for",
77            default="17:00",
78            help="Schedule to update HC package at the given time every day. "
79            "Example: --schedule-for=%%H:%%M")
80        self.arg_parser.add_argument(
81            "--account_id",
82            default=common._DEFAULT_ACCOUNT_ID,
83            help="Partner Android Build account_id to use.")
84        self.arg_parser.add_argument(
85            "--branch", help="Branch to grab the artifact from.")
86        self.arg_parser.add_argument(
87            "--target",
88            help="a comma-separate list of build target product(s).")
89        self.arg_parser.add_argument(
90            "--dest",
91            help="Google Cloud Storage URL to which the file is uploaded.")
92        self.arg_parser.add_argument(
93            "--cancel", help="Cancel all scheduled release if given.")
94        self.arg_parser.add_argument(
95            "--print-all", help="Print all scheduled timers.")
96        self.arg_parser.add_argument(
97            "--additional_files_bucket",
98            default="gs://vtslab-release",
99            help="GCS bucket URL from where to fetch the additional files "
100            "required for HC to run properly.")
101
102    # @Override
103    def Run(self, arg_line):
104        """Schedule a host_constroller package release at a certain time."""
105        args = self.arg_parser.ParseLine(arg_line)
106
107        if args.print_all:
108            logging.info(self._timers)
109            return
110
111        if not args.cancel:
112            if args.schedule_for == "now":
113                self.ReleaseCallback(args.schedule_for, args.account_id,
114                                     args.branch, args.target, args.dest,
115                                     args.additional_files_bucket)
116                return
117
118            elif len(args.schedule_for.split(":")) != 2:
119                logging.error("The format of --schedule-for flag is %H:%M")
120                return False
121
122            if (int(args.schedule_for.split(":")[0]) not in range(24)
123                    or int(args.schedule_for.split(":")[-1]) not in range(60)):
124                logging.error("The value of --schedule-for flag must be in "
125                              "\"00:00\"..\"23:59\" inclusive")
126                return False
127
128            if not args.schedule_for in self._timers:
129                delta_time = datetime.datetime.now().replace(
130                    hour=int(args.schedule_for.split(":")[0]),
131                    minute=int(args.schedule_for.split(":")[-1]),
132                    second=0,
133                    microsecond=0) - datetime.datetime.now()
134
135                if delta_time <= datetime.timedelta(0):
136                    delta_time += datetime.timedelta(days=1)
137
138                self._timers[args.schedule_for] = threading.Timer(
139                    delta_time.total_seconds(), self.ReleaseCallback,
140                    (args.schedule_for, args.account_id, args.branch,
141                     args.target, args.dest, args.additional_files_bucket))
142                self._timers[args.schedule_for].daemon = True
143                self._timers[args.schedule_for].start()
144                logging.info("Release job scheduled for {}".format(
145                    datetime.datetime.now() + delta_time))
146        else:
147            self.CancelAllEvents()
148
149    def FetchVtslab(self, account_id, branch, target, bucket):
150        """Fetchs android-vtslab.zip and return the fetched file path.
151
152        Args:
153            account_id: string, Partner Android Build account_id to use.
154            branch: string, branch to grab the artifact from.
155            targets: string, a comma-separate list of build target product(s).
156            bucket: string, GCS bucket URL from where to fetch the additional
157                    files.
158
159        Returns:
160            path to the fetched android-vtslab.zip file. None if the fetching
161            has failed.
162        """
163        self.console.build_provider["pab"].Authenticate()
164        fetched_path = self.console.build_provider[
165            "pab"].FetchLatestBuiltHCPackage(account_id, branch, target)
166
167        with zipfile.ZipFile(fetched_path, mode="a") as vtslab_package:
168            if _VERSION_INFO_FILE_PATH in vtslab_package.namelist():
169                self._vtslab_package_version = vtslab_package.open(
170                    _VERSION_INFO_FILE_PATH).readline().strip()
171            else:
172                self._vtslab_package_version = ""
173
174            for path in _REPACKAGE_ADDITIONAL_FILE_LIST:
175                additional_file = os.path.join(bucket, path)
176                self.console.build_provider["gcs"].Fetch(additional_file)
177                try:
178                    logging.info("Adding file %s into %s" %
179                                 (os.path.basename(path),
180                                  os.path.basename(fetched_path)))
181                    additional_file_path = self.console.build_provider[
182                        "gcs"].GetAdditionalFile(os.path.basename(path))
183                    vtslab_package.write(additional_file_path, path)
184                except KeyError as e:
185                    logging.exception(e)
186
187            for bin in _REPACKAGE_ADDITIONAL_BIN_LIST:
188                additional_bin = os.path.join(bucket, bin)
189                self.console.build_provider["gcs"].Fetch(additional_bin)
190                try:
191                    logging.info("Adding executable %s into %s" %
192                                 (os.path.basename(bin),
193                                  os.path.basename(fetched_path)))
194                    additional_bin_path = self.console.build_provider[
195                        "gcs"].GetAdditionalFile(os.path.basename(bin))
196                    bin_mode = os.stat(additional_bin_path).st_mode
197                    os.chmod(
198                        additional_bin_path,
199                        bin_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
200                    vtslab_package.write(additional_bin_path, bin)
201                except KeyError as e:
202                    logging.exception(e)
203
204        return fetched_path
205
206    def UploadVtslab(self, package_file_path, dest_path):
207        """upload repackaged vtslab package to GCS.
208
209        Args:
210            package_file_path: string, path to the vtslab package file.
211            dest_path: string, URL to GCS.
212        """
213        if dest_path and dest_path.endswith("/"):
214            split_list = os.path.basename(package_file_path).split(".")
215            if self._vtslab_package_version:
216                try:
217                    timestamp, hash = self._vtslab_package_version.split(":")
218                    split_list[0] += "-%s-%s" % (timestamp, hash)
219                except ValueError as e:
220                    logging.exception(e)
221                    split_list[0] += "-{timestamp_date}"
222            else:
223                split_list[0] += "-{timestamp_date}"
224            dest_path += ".".join(split_list)
225
226        upload_command = "upload --src %s --dest %s" % (package_file_path,
227                                                        dest_path)
228        self.console.onecmd(upload_command)
229
230    def ReleaseCallback(self, schedule_for, account_id, branch, target, dest,
231                        bucket):
232        """Target function for the scheduled Timer.
233
234        Args:
235            schedule_for: string, scheduled time for this Timer.
236                          Format: "%H:%M" (from "00:00" to  "23:59" inclusive)
237            account_id: string, Partner Android Build account_id to use.
238            branch: string, branch to grab the artifact from.
239            targets: string, a comma-separate list of build target product(s).
240            dest: string, URL to GCS.
241            bucket: string, GCS bucket URL from where to fetch the additional
242                    files.
243        """
244        fetched_path = self.FetchVtslab(account_id, branch, target, bucket)
245        if fetched_path:
246            self.UploadVtslab(fetched_path, dest)
247
248        if schedule_for != "now":
249            delta_time = datetime.datetime.now().replace(
250                hour=int(schedule_for.split(":")[0]),
251                minute=int(schedule_for.split(":")[-1]),
252                second=0,
253                microsecond=0) - datetime.datetime.now() + datetime.timedelta(
254                    days=1)
255            self._timers[schedule_for] = threading.Timer(
256                delta_time.total_seconds(), self.ReleaseCallback,
257                (schedule_for, account_id, branch, target, dest, bucket))
258            self._timers[schedule_for].daemon = True
259            self._timers[schedule_for].start()
260            logging.info("Release job scheduled for {}".format(
261                datetime.datetime.now() + delta_time))
262
263    def CancelAllEvents(self):
264        """Cancel all scheduled Timer."""
265        for scheduled_time in self._timers:
266            self._timers[scheduled_time].cancel()
267        self._timers = {}
268