~aleteoryx/muditaos

9cf04ca2f0d76099b1a6a8a74af3f15dbe799426 — Radoslaw Wicik 5 years ago d461391
[EGD-4627] Add target for ecooboot.bin download

ecoboot.bin - bootloader is required in final package.
We will not build it here, we just download newest available
in github releases.
M .gitignore => .gitignore +2 -0
@@ 50,3 50,5 @@ docker/assets/requirements*
*.swo
tags
.ycm_extra_conf.py

update/

M CMakeLists.txt => CMakeLists.txt +5 -0
@@ 278,6 278,11 @@ if (${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo")
        )
endif()

add_custom_target(ecoboot.bin 
    COMMAND ${CMAKE_SOURCE_DIR}/tools/download_asset.py -w ${CMAKE_BINARY_DIR}/update ecoboot download
    BYPRODUCTS update/ecoboot.bin
    )

message_serial_status()

option (BUILD_DOC_WITH_ALL "Build documentation" OFF)

M config/bootstrap_config => config/bootstrap_config +4 -1
@@ 49,6 49,9 @@ INSTALL_PACKAGES="
        wget \
        python3-magic \
        python3-pip \
        libfdisk-dev
        libfdisk-dev \
        python3-requests \
        libfuse-dev \
        libblkid-dev 
"


M config/requirements.txt => config/requirements.txt +1 -0
@@ 1,1 1,2 @@
gitpython==3.1.11
tqdm==4.54.1

A doc/download_assets.md => doc/download_assets.md +85 -0
@@ 0,0 1,85 @@
# Download Assets 

Building update packages requires some external assets.
For now we need only `ecoboot.bin`.
This file may be manually downloaded from [ecoboot releases page](https://github.com/mudita/ecoboot/releases),
from the "Assets" section. This requires the user to log in to GitHub and click inside the web browser, which is a "no go" for automated builds.
To automate this process we have introduced a tool `tools/download_asset.py`.

## GitHub API token

To use GitHub API your GitHub login and special token (API password) are required.
This token can by generated on "User" -> [Settings](https://github.com/settings/profile) page in section [Developer settings](https://github.com/settings/apps) -> [Personal acces tokens](https://github.com/settings/tokens) section.
For more info please refer to the Github help page about adding tokens: [Creating a personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)
### Scopes
When you generate a token you can set "Scopes" for which the token will work.
For downloading assets, we need a ":repo" scope with all sub-scopes.

**Note:** Tokens are displayed only during their creation, so this is the only moment you can copy them.
If you forget to do this, you will have to generate a new token.

## Storing settings
The `download_assets.py` file can have `login` and `password` passed through parameters,
or this data can be stored in your current git repository configuration.

Adding GitHub login to:
```bash
git config --add user.githublogin <login>
```

Adding API Token:
```bash
git config --add user.apitoken
```

Checking the values:
```bash
git config user.githublogin
git config user.apitoken
```

## How to use `download_asset.py`

```bash
$./tools/download_asset.py --help
usage: download_asset.py [-h] [-w WORKDIR] [-t TOKEN] [-l LOGIN] repository {list,ll,download,dw} ...
```

To work properly, `download_asset.py` requires <Login> and <Token>. If they are stored in repo config these
parameters are not required, otherwise, our tool will protest if parameters are not set.
If parameters are set in `.git/config` and You still add these values, they will override the ones in the config.

This tool downloads assets only from our "mudita" organisation in GitHub, but we can change the repository,
this is `repository` parameter, the second required parameter is a task to be completed:
Tasks:
* `list` or `ll`     - list releases, first column is a tag, which may be used in download command.
* `download` or `dw` - downloads first asset in release (if no tag is added, the latest release is used).

## Usage example

List releases:
```bash
./tools/download_asset.py ecoboot ll
 tag | name | date | pre-release
--------------------------------
1.0.4 | Bug fixes for checksum handling | 2020-08-27T13:26:11Z | True
1.0.1 |  | 2020-08-12T09:45:53Z | False
1.0.3 | 1.0.3 JSON to ini migration | 2020-07-28T13:29:56Z | False
0.1 | Support for 2 partitions and boot.ini | 2020-07-07T14:48:18Z | False
05_07_2019 | Working with PurePhone | 2019-07-05T11:36:51Z | True
```

Download latest `ecoboot.bin`
```bash
./tools/download_asset.py ecoboot dw
```

Download `ecoboot.bin` for tag `1.0.3`
```bash
./tools/download_asset.py ecoboot dw 1.0.3
```

Download the latest MuditaOS package to the `packages` directory with a custom login and API token
```bash
./tools/download_asset.py -w packages -l someUser -t secreetToken123123123 MuditaOs dw
```

M docker/docker-compose.yml => docker/docker-compose.yml +3 -3
@@ 1,21 1,21 @@
version: '3'
services:
 gh-runner0:
   image: wearemudita/mudita_os_builder:1.4
   image: wearemudita/mudita_os_builder:1.6
   environment:
     WORKER_NAME: PureBuilder0
   env_file:
     - runner_settings
   entrypoint: /cmd.sh
 gh-runner1:
   image: wearemudita/mudita_os_builder:1.4
   image: wearemudita/mudita_os_builder:1.6
   environment:
     WORKER_NAME: PureBuilder1
   env_file:
     - runner_settings
   entrypoint: /cmd.sh
 gh-runner2:
   image: wearemudita/mudita_os_builder:1.4
   image: wearemudita/mudita_os_builder:1.6
   environment:
     WORKER_NAME: PureBuilder2
   env_file:

M in_docker.sh => in_docker.sh +1 -1
@@ 3,7 3,7 @@
# For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

CONTAINER_NAME="wearemudita/mudita_os_builder"
CONTAINER_TAG="1.4"
CONTAINER_TAG="1.5"
CONTAINER=${CONTAINER_NAME}:${CONTAINER_TAG}
PURE_HOME=`pwd`
STANDARD_OPTIONS="-v `pwd`:${PURE_HOME} --user \"$(id -u):$(id -g)\" --env HOME=${PURE_HOME} -t"

A tools/download_asset.py => tools/download_asset.py +193 -0
@@ 0,0 1,193 @@
#!/usr/bin/env python3
"""
Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

Download ecooboot.bin from repository

@package globalFinder
"""

import argparse
import git
import json
import os
import requests
import sys
from tqdm import tqdm


class Getter(object):
    '''Download latest ecooboot.bin image'''

    def __init__(self):
        self.host = 'https://api.github.com/repos'
        self.organisation = 'mudita'
        self.repo = 'ecoboot'
        self.restPrefix = self.host
        self.restPrefix += '/'
        self.restPrefix += self.organisation
        self.restPrefix += '/'
        self.restPrefix += self.repo
        self.restPrefix += '/'
        self.apitoken = None
        self.ghLogin = None
        self.getGitRoot()
        self.getApiToken()
        self.getGHLogin()
        self.releases = []
        self.workdir = ""

    def getGitRoot(self):
        'Find git root directory'
        self.gitRepo = git.Repo(os.getcwd(), search_parent_directories=True)
        self.gitRoot = self.gitRepo.git.rev_parse("--show-toplevel")
        return self.gitRoot

    def getApiToken(self, args=None):
        if args is not None:
            if args.token is not None:
                self.apitoken = args.token
                return
        try:
            gitConfigReader = self.gitRepo.config_reader()
            self.apitoken = gitConfigReader.get_value("user", "apitoken")
        except git.exc.NoOptionError as error:
            pass

    def getGHLogin(self, args=None):
        if args is not None:
            if args.login is not None:
                self.ghLogin = args.login
                return
        try:
            gitConfigReader = self.gitRepo.config_reader()
            self.ghLogin = gitConfigReader.get_value("user", "githublogin")
        except git.exc.NoOptionError as error:
            pass

    def findWorkDir(self):
        '''Finds and sets current workdir (based on 'workdir' argument)'''
        if os.path.isabs(self.args.workdir):
            self.workdir = self.args.workdir
        else:
            self.workdir = self.getGitRoot() + "/" + self.args.workdir

    def getReleases(self, args):
        '''List only releases'''
        self.args = args
        self.getGHLogin(args)
        self.getApiToken(args)
        request = self.restPrefix + "releases"
        headers = {'accept': 'application/vnd.github.v3+json'}
        page = 0
        itemsOnPage = 100
        count = itemsOnPage
        while count == itemsOnPage:
            page += 1
            queryParams = {'per_page': itemsOnPage, 'page': page}
            response = requests.get(request, auth=(self.ghLogin, self.apitoken), headers=headers, params=queryParams)
            if response.status_code != requests.codes.ok:
                print("download error:", response.status_code)
                print(response.content)
                sys.exit(1)
            items = json.loads(response.content)
            count = len(items)
            self.releases += items
        self.releases.sort(key=lambda r: r['published_at'], reverse=True)

    def listReleases(self, args):
        self.getReleases(args)
        print(" tag | name | date | pre-release")
        print("--------------------------------")
        for release in self.releases:
            print(release['tag_name'], release['name'], release['published_at'], release['prerelease'], sep=" | ")

    def downloadRelease(self, args):
        print(sys._getframe().f_code.co_name)
        self.getReleases(args)
        print(args.tag)
        release = None
        if args.tag is None:
            release = self.releases[0]
        else:
            for rel in self.releases:
                if rel['tag_name'] == args.tag:
                    release = rel
                    break
        if release is None:
            print("No release with tag:", args.tag)
        assets = release['assets']
        self.downloadAsset(assets[0])

    def downloadAsset(self, asset):
        self.createWorkdir()
        print("name:", asset['name'])
        print("url:", asset['url'])
        print("size:", asset['size'])
        headers = {'accept': 'application/octet-stream'}
        response = requests.get(asset['url'],
                                auth=(self.ghLogin, self.apitoken),
                                headers=headers,
                                stream=True)
        progres_bar = tqdm(total=asset['size'], unit='iB', unit_scale=True)
        with open(self.args.workdir + "/" + asset['name'], 'wb') as fd:
            for chunk in response.iter_content(chunk_size=1024):
                progres_bar.update(len(chunk))
                fd.write(chunk)
        if response.status_code != requests.codes.ok:
            print("download error:", response.status_code)
            print(response.content)
            sys.exit(2)

    def createWorkdir(self):
        from pathlib import Path
        Path(self.workdir).mkdir(parents=True, exist_ok=True)

    def run(self, args):
        self.args = args
        self.findWorkDir()
        print(self.args)


def main():
    getter = Getter()

    parser = argparse.ArgumentParser(description="Download ecooboot")
    parser.add_argument('-w', '--workdir', help="Directory where package is build", default="update")
    parser.add_argument('-t', '--token',
                        help="GitHub security token "
                             "by default read from `git config user.apitoken` "
                             "Generate it on https://github.com/settings/tokens "
                             "Set privileges to `repo` "
                             "and store with command: `git config -add user.apitoken <token from GH>",
                        required=(getter.apitoken is None))
    parser.add_argument('-l', '--login',
                        help="GitHub login"
                             "by default read from `git config user.githublogin`"
                             "to add your login use:"
                             "`git config -add user.githublogin <login>`",
                        required=(getter.ghLogin is None))

    parser.add_argument('repository', help="repository name from which assets will be downloaded")

    parser.set_defaults(func=getter.run)
    subparsers = parser.add_subparsers(title="commands",
                                       description="available commands")
    listReleases_args = subparsers.add_parser('list', aliases=['ll'],
                                              description="List public releases")
    listReleases_args.set_defaults(func=getter.listReleases)

    getReleases_args = subparsers.add_parser('download', aliases=['dw'],
                                             description="Download Release based on tag or the latest")
    getReleases_args.set_defaults(func=getter.downloadRelease)
    getReleases_args.add_argument('tag', help="Download release with selected tag", nargs='?')

    args = parser.parse_args()
    getter.repo = args.repository
    getter.workdir = args.workdir
    args.func(args)


if __name__ == "__main__":
    main()