From 64edb1a35bf788a06dffd64658df0218275a8e52 Mon Sep 17 00:00:00 2001 From: Johan Lorenzo Date: Wed, 27 Mar 2019 12:06:59 +0100 Subject: [PATCH] Refactor decision_task.py to match android-components' --- .taskcluster.yml | 12 +- automation/taskcluster/decision_task.py | 116 +++++++ .../taskcluster/decision_task_nightly.py | 172 ---------- automation/taskcluster/lib/tasks.py | 322 ++++++++++++------ automation/taskcluster/lib/util.py | 20 ++ .../taskcluster/schedule_nightly_graph.py | 1 - 6 files changed, 357 insertions(+), 286 deletions(-) create mode 100644 automation/taskcluster/decision_task.py delete mode 100644 automation/taskcluster/decision_task_nightly.py create mode 100644 automation/taskcluster/lib/util.py diff --git a/.taskcluster.yml b/.taskcluster.yml index 0ecf6f949..50d9cfca3 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -54,10 +54,6 @@ tasks: $if: 'is_repo_trusted' then: mobile-3-decision else: mobile-1-decision - build_worker_type: - $if: 'is_repo_trusted' - then: mobile-3-b-fenix - else: mobile-1-b-fenix track: $if: 'is_repo_trusted' @@ -104,10 +100,10 @@ tasks: TASK_ID: ${decision_task_id} TASKS_PRIORITY: ${tasks_priority} SCHEDULER_ID: ${scheduler_id} - BUILD_WORKER_TYPE: ${build_worker_type} MOBILE_HEAD_REPOSITORY: ${repository} MOBILE_HEAD_BRANCH: ${head_branch} MOBILE_HEAD_REV: ${head_rev} + MOBILE_TRIGGERED_BY: ${user} features: taskclusterProxy: true extra: @@ -130,14 +126,14 @@ tasks: features: taskclusterProxy: true chainOfTrust: true - env: - MOBILE_TRIGGERED_BY: ${user} command: - >- git fetch ${repository} ${head_branch} && git config advice.detachedHead false && git checkout ${head_rev} - && python automation/taskcluster/decision_task_nightly.py \ + && python automation/taskcluster/decision_task.py \ + release \ + --nightly \ --track ${track} \ --commit \ --output /opt/fenix/app/build/outputs/apk \ diff --git a/automation/taskcluster/decision_task.py b/automation/taskcluster/decision_task.py new file mode 100644 index 000000000..6694875a2 --- /dev/null +++ b/automation/taskcluster/decision_task.py @@ -0,0 +1,116 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +Decision task for nightly releases. +""" + +from __future__ import print_function + +import argparse +import os +import taskcluster + +from lib.tasks import TaskBuilder, schedule_task_graph +from lib.util import ( + populate_chain_of_trust_task_graph, + populate_chain_of_trust_required_but_unused_files +) + +REPO_URL = os.environ.get('MOBILE_HEAD_REPOSITORY') +COMMIT = os.environ.get('MOBILE_HEAD_REV') +PR_TITLE = os.environ.get('GITHUB_PULL_TITLE', '') + +# If we see this text inside a pull request title then we will not execute any tasks for this PR. +SKIP_TASKS_TRIGGER = '[ci skip]' + + +BUILDER = TaskBuilder( + task_id=os.environ.get('TASK_ID'), + repo_url=os.environ.get('MOBILE_HEAD_REPOSITORY'), + branch=os.environ.get('MOBILE_HEAD_BRANCH'), + commit=COMMIT, + owner="fenix-eng-notifications@mozilla.com", + source='{}/raw/{}/.taskcluster.yml'.format(REPO_URL, COMMIT), + scheduler_id=os.environ.get('SCHEDULER_ID', 'taskcluster-github'), + tasks_priority=os.environ.get('TASKS_PRIORITY'), +) + + +def nightly(apks, track, commit, date_string): + is_staging = track == 'staging-nightly' + + build_tasks = {} + signing_tasks = {} + push_tasks = {} + artifacts = ["public/{}".format(os.path.basename(apk)) for apk in apks] + + build_task_id = taskcluster.slugId() + build_tasks[build_task_id] = BUILDER.craft_assemble_release_task(apks, is_staging) + + signing_task_id = taskcluster.slugId() + signing_tasks[signing_task_id] = BUILDER.craft_signing_task( + build_task_id, + apks=artifacts, + date_string=date_string, + is_staging=is_staging, + ) + + push_task_id = taskcluster.slugId() + push_tasks[push_task_id] = BUILDER.craft_push_task( + signing_task_id, + apks=artifacts, + commit=commit, + is_staging=is_staging + ) + + return (build_tasks, signing_tasks, push_tasks) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description='Creates and submit a graph of tasks on Taskcluster.' + ) + + subparsers = parser.add_subparsers(dest='command') + + subparsers.add_parser('pr-or-push') + release_parser = subparsers.add_parser('release') + + release_parser.add_argument('--nightly', action="store_true", default=False) + release_parser.add_argument( + '--track', action="store", choices=['nightly', 'staging-nightly'], required=True + ) + release_parser.add_argument( + '--commit', action="store_true", help="commit the google play transaction" + ) + release_parser.add_argument( + '--apk', dest="apks", metavar="path", action="append", + help="Path to APKs to sign and upload", required=True + ) + release_parser.add_argument( + '--output', metavar="path", action="store", help="Path to the build output", required=True + ) + release_parser.add_argument('--date', action="store", help="ISO8601 timestamp for build") + + result = parser.parse_args() + + command = result.command + + if command == 'pr-or-push': + # TODO + ordered_groups_of_tasks = {} + elif command == 'release': + apks = ["{}/{}".format(result.output, apk) for apk in result.apks] + # nightly(apks, result.track, result.commit, result.date) + ordered_groups_of_tasks = nightly( + apks, result.track, result.commit, result.date + ) + else: + raise Exception('Unsupported command "{}"'.format(command)) + + full_task_graph = schedule_task_graph(ordered_groups_of_tasks) + + populate_chain_of_trust_task_graph(full_task_graph) + populate_chain_of_trust_required_but_unused_files() diff --git a/automation/taskcluster/decision_task_nightly.py b/automation/taskcluster/decision_task_nightly.py deleted file mode 100644 index 460e523af..000000000 --- a/automation/taskcluster/decision_task_nightly.py +++ /dev/null @@ -1,172 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -""" -Decision task for nightly releases. -""" - -from __future__ import print_function - -import argparse -import arrow -import json -import lib.tasks -import os -import taskcluster - -TASK_ID = os.environ.get('TASK_ID') -SCHEDULER_ID = os.environ.get('SCHEDULER_ID') -GITHUB_HTTP_REPOSITORY = os.environ.get('MOBILE_HEAD_REPOSITORY') -HEAD_REV = os.environ.get('MOBILE_HEAD_REV') -HEAD_BRANCH = os.environ.get('MOBILE_HEAD_BRANCH') - -BUILDER = lib.tasks.TaskBuilder( - task_id=TASK_ID, - owner="fenix-eng-notifications@mozilla.com", - source='{}/raw/{}/.taskcluster.yml'.format(GITHUB_HTTP_REPOSITORY, HEAD_REV), - scheduler_id=SCHEDULER_ID, - build_worker_type=os.environ.get('BUILD_WORKER_TYPE'), -) - - -def generate_build_task(apks, is_staging): - artifacts = {'public/{}'.format(os.path.basename(apk)): { - "type": 'file', - "path": apk, - "expires": taskcluster.stringDate(taskcluster.fromNow('1 year')), - } for apk in apks} - - checkout = ( - "export TERM=dumb && git fetch {} {} --tags && " - "git config advice.detachedHead false && " - "git checkout {}".format( - GITHUB_HTTP_REPOSITORY, HEAD_BRANCH, HEAD_REV - ) - ) - sentry_secret = '{}project/mobile/fenix/sentry'.format('garbage/staging/' if is_staging else '') - leanplum_secret = '{}project/mobile/fenix/leanplum'.format('garbage/staging/' if is_staging else '') - - return taskcluster.slugId(), BUILDER.build_task( - name="(Fenix) Build task", - description="Build Fenix from source code.", - command=( - checkout + - ' && python automation/taskcluster/helper/get-secret.py' - ' -s {} -k dsn -f .sentry_token'.format(sentry_secret) + - ' && python automation/taskcluster/helper/get-secret.py' - ' -s {} -k production -f .leanplum_token'.format(leanplum_secret) + - ' && ./gradlew --no-daemon -PcrashReports=true clean test assembleRelease'), - features={ - "chainOfTrust": True, - "taskclusterProxy": True - }, - artifacts=artifacts, - scopes=[ - "secrets:get:{}".format(sentry_secret), - "secrets:get:{}".format(leanplum_secret) - ], - routes=["notify.email.fenix-eng-notifications@mozilla.com.on-failed"] - ) - - -def generate_signing_task(build_task_id, apks, date, is_staging): - artifacts = ["public/{}".format(os.path.basename(apk)) for apk in apks] - - signing_format = 'autograph_apk' - index_release = 'staging-signed-nightly' if is_staging else 'signed-nightly' - routes = [ - "index.project.mobile.fenix.{}.nightly.{}.{}.{}.latest".format(index_release, date.year, date.month, date.day), - "index.project.mobile.fenix.{}.nightly.{}.{}.{}.revision.{}".format(index_release, date.year, date.month, date.day, HEAD_REV), - "index.project.mobile.fenix.{}.nightly.latest".format(index_release), - ] - scopes = [ - "project:mobile:fenix:releng:signing:format:{}".format(signing_format), - "project:mobile:fenix:releng:signing:cert:{}".format('dep-signing' if is_staging else 'release-signing') - ] - - return taskcluster.slugId(), BUILDER.craft_signing_task( - build_task_id, - name="(Fenix) Signing task", - description="Sign release builds of Fenix", - apks=artifacts, - scopes=scopes, - routes=routes, - signing_format=signing_format, - is_staging=is_staging - ) - - -def generate_push_task(signing_task_id, apks, commit, is_staging): - artifacts = ["public/{}".format(os.path.basename(apk)) for apk in apks] - - return taskcluster.slugId(), BUILDER.craft_push_task( - signing_task_id, - name="(Fenix) Push task", - description="Upload signed release builds of Fenix to Google Play", - apks=artifacts, - scopes=[ - "project:mobile:fenix:releng:googleplay:product:fenix{}".format(':dep' if is_staging else '') - ], - commit=commit, - is_staging=is_staging - ) - - -def populate_chain_of_trust_required_but_unused_files(): - # These files are needed to keep chainOfTrust happy. However, they have no need for Fenix - # at the moment. For more details, see: https://github.com/mozilla-releng/scriptworker/pull/209/files#r184180585 - - for file_name in ('actions.json', 'parameters.yml'): - with open(file_name, 'w') as f: - json.dump({}, f) - - -def nightly(apks, track, commit, date_string): - queue = taskcluster.Queue({'baseUrl': 'http://taskcluster/queue/v1'}) - date = arrow.get(date_string) - is_staging = track == 'staging-nightly' - - task_graph = {} - - build_task_id, build_task = generate_build_task(apks, is_staging) - lib.tasks.schedule_task(queue, build_task_id, build_task) - - task_graph[build_task_id] = {} - task_graph[build_task_id]['task'] = queue.task(build_task_id) - - sign_task_id, sign_task = generate_signing_task(build_task_id, apks, date, is_staging) - lib.tasks.schedule_task(queue, sign_task_id, sign_task) - - task_graph[sign_task_id] = {} - task_graph[sign_task_id]['task'] = queue.task(sign_task_id) - - push_task_id, push_task = generate_push_task(sign_task_id, apks, commit, is_staging) - lib.tasks.schedule_task(queue, push_task_id, push_task) - - task_graph[push_task_id] = {} - task_graph[push_task_id]['task'] = queue.task(push_task_id) - - print(json.dumps(task_graph, indent=4, separators=(',', ': '))) - - with open('task-graph.json', 'w') as f: - json.dump(task_graph, f) - - populate_chain_of_trust_required_but_unused_files() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description='Create a release pipeline (build, sign, publish) on taskcluster.') - - parser.add_argument('--track', dest="track", action="store", choices=['nightly', 'staging-nightly'], required=True) - parser.add_argument('--commit', action="store_true", help="commit the google play transaction") - parser.add_argument('--apk', dest="apks", metavar="path", action="append", help="Path to APKs to sign and upload", - required=True) - parser.add_argument('--output', metavar="path", action="store", help="Path to the build output", - required=True) - parser.add_argument('--date', action="store", help="ISO8601 timestamp for build") - - result = parser.parse_args() - apks = ["{}/{}".format(result.output, apk) for apk in result.apks] - nightly(apks, result.track, result.commit, result.date) diff --git a/automation/taskcluster/lib/tasks.py b/automation/taskcluster/lib/tasks.py index bf4df70f1..4d5011874 100644 --- a/automation/taskcluster/lib/tasks.py +++ b/automation/taskcluster/lib/tasks.py @@ -2,143 +2,255 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +from __future__ import print_function + +import arrow import datetime import json +import os import taskcluster +DEFAULT_EXPIRES_IN = '1 year' + class TaskBuilder(object): - def __init__(self, task_id, owner, source, scheduler_id, build_worker_type): + def __init__( + self, task_id, repo_url, branch, commit, owner, source, scheduler_id, + tasks_priority='lowest' + ): self.task_id = task_id + self.repo_url = repo_url + self.branch = branch + self.commit = commit self.owner = owner self.source = source self.scheduler_id = scheduler_id - self.build_worker_type = build_worker_type + self.tasks_priority = tasks_priority - def build_task(self, name, description, command, artifacts, features, scopes=[], routes=[]): + def craft_assemble_release_task(self, apks, is_staging=False): + artifacts = { + 'public/{}'.format(os.path.basename(apk)): { + "type": 'file', + "path": apk, + "expires": taskcluster.stringDate(taskcluster.fromNow(DEFAULT_EXPIRES_IN)), + } + for apk in apks + } + + sentry_secret = '{}project/mobile/fenix/sentry'.format( + 'garbage/staging/' if is_staging else '' + ) + leanplum_secret = '{}project/mobile/fenix/leanplum'.format( + 'garbage/staging/' if is_staging else '' + ) + + pre_gradle_commands = ( + 'python automation/taskcluster/helper/get-secret.py -s {} -k {} -f {}'.format( + secret, key, target_file + ) + for secret, key, target_file in ( + (sentry_secret, 'dsn', '.sentry_token'), + (leanplum_secret, 'production', '.leanplum_token'), + ) + ) + + gradle_commands = ( + './gradlew --no-daemon -PcrashReports=true clean test assembleRelease', + ) + + command = ' && '.join( + cmd + for commands in (pre_gradle_commands, gradle_commands) + for cmd in commands + if cmd + ) + + return self._craft_build_ish_task( + name='Fenix - Build task', + description='Build Fenix from source code', + command=command, + scopes=[ + "secrets:get:{}".format(secret) for secret in (sentry_secret, leanplum_secret) + ], + artifacts=artifacts, + routes=["notify.email.fenix-eng-notifications@mozilla.com.on-failed"], + is_staging=is_staging, + ) + + def _craft_build_ish_task( + self, name, description, command, dependencies=None, artifacts=None, scopes=None, + routes=None, is_staging=True + ): + dependencies = [] if dependencies is None else dependencies + artifacts = {} if artifacts is None else artifacts + scopes = [] if scopes is None else scopes + routes = [] if routes is None else routes + + checkout_command = ( + "export TERM=dumb && " + "git fetch {} {} --tags && " + "git config advice.detachedHead false && " + "git checkout {}".format( + self.repo_url, self.branch, self.commit + ) + ) + + command = '{} && {}'.format(checkout_command, command) + + features = {} + if artifacts: + features['chainOfTrust'] = True + if any(scope.startswith('secrets:') for scope in scopes): + features['taskclusterProxy'] = True + + payload = { + "features": features, + "maxRunTime": 7200, + "image": "mozillamobile/fenix:1.3", + "command": [ + "/bin/bash", + "--login", + "-cx", + command + ], + "artifacts": artifacts, + } + + return self._craft_default_task_definition( + 'mobile-1-b-fenix' if is_staging else 'mobile-3-b-fenix', + 'aws-provisioner-v1', + dependencies, + routes, + scopes, + name, + description, + payload + ) + + def _craft_default_task_definition( + self, worker_type, provisioner_id, dependencies, routes, scopes, name, description, payload + ): created = datetime.datetime.now() - expires = taskcluster.fromNow('1 year') deadline = taskcluster.fromNow('1 day') + expires = taskcluster.fromNow(DEFAULT_EXPIRES_IN) return { - "workerType": self.build_worker_type, + "provisionerId": provisioner_id, + "workerType": worker_type, "taskGroupId": self.task_id, "schedulerId": self.scheduler_id, + "created": taskcluster.stringDate(created), + "deadline": taskcluster.stringDate(deadline), "expires": taskcluster.stringDate(expires), "retries": 5, - "created": taskcluster.stringDate(created), "tags": {}, - "priority": "lowest", - "deadline": taskcluster.stringDate(deadline), - "dependencies": [self.task_id], + "priority": self.tasks_priority, + "dependencies": [self.task_id] + dependencies, + "requires": "all-completed", "routes": routes, "scopes": scopes, - "requires": "all-completed", - "payload": { - "features": features, - "maxRunTime": 7200, - "image": "mozillamobile/fenix:1.3", - "command": [ - "/bin/bash", - "--login", - "-c", - command - ], - "artifacts": artifacts, - "deadline": taskcluster.stringDate(deadline) - }, - "provisionerId": "aws-provisioner-v1", + "payload": payload, "metadata": { "name": name, "description": description, "owner": self.owner, - "source": self.source - } - } - - def craft_signing_task(self, build_task_id, name, description, signing_format, is_staging, apks, scopes, routes): - created = datetime.datetime.now() - expires = taskcluster.fromNow('1 year') - deadline = taskcluster.fromNow('1 day') - - return { - "workerType": 'mobile-signing-dep-v1' if is_staging else 'mobile-signing-v1', - "taskGroupId": self.task_id, - "schedulerId": self.scheduler_id, - "expires": taskcluster.stringDate(expires), - "retries": 5, - "created": taskcluster.stringDate(created), - "tags": {}, - "priority": "lowest", - "deadline": taskcluster.stringDate(deadline), - "dependencies": [self.task_id, build_task_id], - "routes": routes, - "scopes": scopes, - "requires": "all-completed", - "payload": { - "maxRunTime": 3600, - "upstreamArtifacts": [ - { - "paths": apks, - "formats": [signing_format], - "taskId": build_task_id, - "taskType": "build" - } - ] + "source": self.source, }, - "provisionerId": "scriptworker-prov-v1", - "metadata": { - "name": name, - "description": description, - "owner": self.owner, - "source": self.source - } } - def craft_push_task(self, signing_task_id, name, description, is_staging, apks, scopes, commit): - created = datetime.datetime.now() - expires = taskcluster.fromNow('1 year') - deadline = taskcluster.fromNow('1 day') - - return { - "workerType": 'mobile-pushapk-dep-v1' if is_staging else 'mobile-pushapk-v1', - "taskGroupId": self.task_id, - "schedulerId": self.scheduler_id, - "expires": taskcluster.stringDate(expires), - "retries": 5, - "created": taskcluster.stringDate(created), - "tags": {}, - "priority": "lowest", - "deadline": taskcluster.stringDate(deadline), - "dependencies": [self.task_id, signing_task_id], - "routes": [], - "scopes": scopes, - "requires": "all-completed", - "payload": { - "commit": commit, - "google_play_track": 'nightly', - "upstreamArtifacts": [ - { - "paths": apks, - "taskId": signing_task_id, - "taskType": "signing" - } - ] - }, - "provisionerId": "scriptworker-prov-v1", - "metadata": { - "name": name, - "description": description, - "owner": self.owner, - "source": self.source - } + def craft_signing_task( + self, build_task_id, apks, date_string, is_staging=True, + ): + date = arrow.get(date_string) + signing_format = 'autograph_apk' + payload = { + "upstreamArtifacts": [{ + "paths": apks, + "formats": [signing_format], + "taskId": build_task_id, + "taskType": "build", + }], } + index_release = 'staging-signed-nightly' if is_staging else 'signed-nightly' + routes = [ + "index.project.mobile.fenix.{}.nightly.{}.{}.{}.latest".format( + index_release, date.year, date.month, date.day + ), + "index.project.mobile.fenix.{}.nightly.{}.{}.{}.revision.{}".format( + index_release, date.year, date.month, date.day, self.commit + ), + "index.project.mobile.fenix.{}.nightly.latest".format(index_release), + ] + + return self._craft_default_task_definition( + worker_type='mobile-signing-dep-v1' if is_staging else 'mobile-signing-v1', + provisioner_id='scriptworker-prov-v1', + dependencies=[build_task_id], + routes=routes, + scopes=[ + "project:mobile:fenix:releng:signing:format:{}".format(signing_format), + "project:mobile:fenix:releng:signing:cert:{}".format( + 'dep-signing' if is_staging else 'release-signing' + ) + ], + name="Fenix - Signing task", + description="Sign release builds of Fenix", + payload=payload + ) + + def craft_push_task( + self, signing_task_id, apks, is_staging=True, commit=False + ): + payload = { + "commit": commit, + "google_play_track": 'nightly', + "upstreamArtifacts": [ + { + "paths": apks, + "taskId": signing_task_id, + "taskType": "signing" + } + ] + } + + return self._craft_default_task_definition( + worker_type='mobile-pushapk-dep-v1' if is_staging else 'mobile-pushapk-v1', + provisioner_id='scriptworker-prov-v1', + dependencies=[signing_task_id], + routes=[], + scopes=[ + "project:mobile:fenix:releng:googleplay:product:fenix{}".format( + ':dep' if is_staging else '' + ) + ], + name="Fenix - Push task", + description="Upload signed release builds of Fenix to Google Play", + payload=payload + ) + def schedule_task(queue, taskId, task): - print "TASK", taskId - print json.dumps(task, indent=4, separators=(',', ': ')) + print("TASK", taskId) + print(json.dumps(task, indent=4, separators=(',', ': '))) result = queue.createTask(taskId, task) - print "RESULT", taskId - print json.dumps(result) + print("RESULT", taskId) + print(json.dumps(result)) + + +def schedule_task_graph(ordered_groups_of_tasks): + queue = taskcluster.Queue({'baseUrl': 'http://taskcluster/queue/v1'}) + full_task_graph = {} + + # TODO: Switch to async python to speed up submission + for group_of_tasks in ordered_groups_of_tasks: + for task_id, task_definition in group_of_tasks.items(): + schedule_task(queue, task_id, task_definition) + + full_task_graph[task_id] = { + # Some values of the task definition are automatically filled. Querying the task + # allows to have the full definition. This is needed to make Chain of Trust happy + 'task': queue.task(task_id), + } + return full_task_graph diff --git a/automation/taskcluster/lib/util.py b/automation/taskcluster/lib/util.py new file mode 100644 index 000000000..7ef50c02f --- /dev/null +++ b/automation/taskcluster/lib/util.py @@ -0,0 +1,20 @@ +import json + + +def populate_chain_of_trust_required_but_unused_files(): + # Thoses files are needed to keep chainOfTrust happy. However, they have no + # need for android-components, at the moment. For more details, see: + # https://github.com/mozilla-releng/scriptworker/pull/209/files#r184180585 + + for file_names in ('actions.json', 'parameters.yml'): + with open(file_names, 'w') as f: + json.dump({}, f) # Yaml is a super-set of JSON. + + +def populate_chain_of_trust_task_graph(full_task_graph): + # taskgraph must follow the format: + # { + # task_id: full_task_definition + # } + with open('task-graph.json', 'w') as f: + json.dump(full_task_graph, f) diff --git a/automation/taskcluster/schedule_nightly_graph.py b/automation/taskcluster/schedule_nightly_graph.py index d0a3ddf74..cf4b5e449 100644 --- a/automation/taskcluster/schedule_nightly_graph.py +++ b/automation/taskcluster/schedule_nightly_graph.py @@ -2,7 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import argparse import datetime import jsone import os