Рубрики
Без рубрики

Развертывание сервера с помощью Python: от А до Я.

В этом руководстве вы узнаете, как настроить сервер и развернуть веб-приложение с нуля, используя только Python.

Автор оригинала: Pietro Grandinetti PhD.

В этом уроке я проиллюстрирую, как настроить сервер для запуска веб-приложения без какого-либо другого инструмента, кроме Python.

В конце его у вас будет:

  • Узнал, каковы компоненты конфигурации сервера для веб-развертывания.
  • Получил воспроизводимый шаблон Python с помощью GitHub gists.

Фон

В те времена, когда Docker еще не существовал, я обычно настраивал серверы в облачной среде с помощью кода Python. В основном это был скрипт/проект Python, который я запускал на своей (локальной) машине, и он выполнял команды на (удаленном) сервере.

Я давно не пользовался этим проектом шахт, но наткнулся на него на прошлой неделе и понял две вещи:

  • Сегодня мне бы не понравилось его использовать. Докер гораздо удобнее.
  • Однако понимание этого кода дает отличное представление о том, как настроить удаленные серверы!

На самом деле, причина, по которой я написал это, заключается в том, что еще раньше я настраивал все серверы вручную: вход через ssh, установка необходимых пакетов и т. Д.

Проделав это несколько раз, я решил автоматизировать процедуру . Вот почему я построил проект, который собираюсь вам показать.

Это означает, что код выделяет детали всего, что вы должны делать на сервере, в отличие от использования Docker, где большинство деталей скрыто.

Поэтому этот пост может быть полезен по двум причинам:

  1. Если вы не хотите или не можете использовать Docker.
  2. Это даст вам довольно хорошее понимание того, что происходит на удаленной машине! Знание всегда имеет значение.

Обзор учебника

Код организован в функции, где каждая функция выполняет задачу на сервере, даже если она выполняется на локальной машине.

Поэтому, прежде чем смотреть на код, стоит подумать о том, какие шаги вам нужно было бы выполнить (вручную), если бы вы не читали этот учебник.

На самом деле, ручное развертывание на сервере vanilla является обязательным для разработчика . По крайней мере, один раз в жизни запустите машину vanilla в любом облачном провайдере (GCP, AWS, Linode, Digital Ocean, Azure и т. Д.) И выполните полноценное производственное развертывание. Хотя бы один раз!!

Вы многому научитесь, делая это вручную. Давайте сначала рассмотрим обзор каждого отдельного шага, а затем еще раз рассмотрим каждый из них вместе с кодом.

Шаг 1 – Базовая конфигурация машины

Во-первых, вы должны обновить все пакеты. Когда вы создаете новый экземпляр, он обычно работает с немного устаревшей версией операционной системы, поэтому вам необходимо обновить пакеты. Например, в Debian/Ubuntu это делается с помощью apt-get update .

После этого вам нужно будет установить все пакеты, относящиеся к вашему проекту. В коде этого урока я установлю множество пакетов, включая postgresql-server , который позволяет запускать веб-приложение, использующее PostgreSQL в бэкэнде.

Затем, поскольку вы хотите запустить приложение Python, вам необходимо настроить Python на сервере.

Большинство операционных систем поставляются с предустановленным двоичным файлом Python по умолчанию. Однако, на мой взгляд, это не очень хорошее решение.

Причина в том, что вы, возможно, разработали свое приложение с Python3.7, но тогда на сервере есть Python3.9. Все рухнет, и будет трудно понять, почему.

Вот почему в моем коде также есть конкретные инструкции по настройке правильной версии Python в 3 подшагах.

  • Загрузите исходный код Python.
  • Скомпилируйте его.
  • Свяжите его как исполняемый файл в машине.

Наконец, как и в большинстве приложений Python, вы захотите создать virtualenv только для этого приложения.

Если в вашем приложении есть выделенный сервер, то есть это единственное программное обеспечение, работающее в нем, вы можете пропустить эту часть. Мой код в любом случае содержит функцию также для установки virtualenvwrapper и настройки virtualenv, предназначенного для этого приложения.

Это конец первого шага.

Шаг 2 – Установите приложение

Чтобы сервер запустил ваше приложение, это должно быть установлено! Если ваше приложение является исполняемым файлом, который можно установить, вам следует загрузить его (через curl или wget ) или скопировать его с локальной машины на удаленный сервер (через scp ).

Более распространенный случай с приложениями Python, по крайней мере для меня, – это когда для запуска приложения нам нужен исходный код. В этом случае вам нужно поместить исходный код на удаленный сервер. Есть два распространенных способа сделать это:

  1. Используя git clone , вы берете исходный код из репозитория git, который находится в Сети.
  2. Используя scp , чтобы скопировать исходный код с локальной машины на удаленную.

В своем коде я буду использовать вариант 1, поэтому я могу показать вам хороший способ, который я нашел тогда, чтобы написать код, который переключается между ветвями и фиксациями в репозитории (например, если вы хотите выполнить откат).

После этого, если у вас есть requirements.txt файл, как и большинство проектов Python, тогда вам придется установить все эти модули Python в virtualenv . Я покажу, как это сделать через минуту.

Шаг 3 – Запустите приложение!

Эта часть является самой простой, особенно если вы используете готовый к работе надежный веб-сервер. Я выбрал Gunicorn, но есть много альтернатив, которые одинаково просты в использовании.

Это три шага, которые вы должны пройти на высоком уровне. Теперь я хочу рассмотреть их более внимательно вместе с вами, но… есть одна недостающая деталь: как мы можем отправлять команды для выполнения на сервер, не выполняя ручной вход через ssh?

Настройка подключения

К счастью для нас, пакет Python под названием fabric решает эту проблему. Все, что вам нужно, – это такая функция, которая создает соединение с сервером:

from os import environ

from fabric import Connection

def create_conn():
    # Switch the two lines if you connect via PEM Key
    # instead of password.
    params = {
        #'key_filename': environ['SSH_KEY_PATH']}
        'password': environ['REMOTE_PASSWORD']
    }
    conn = Connection(
        host=environ['REMOTE_HOST'],
        user=environ['REMOTE_USER'],
        connect_kwargs=params,
    )
    return conn

Объект соединения, возвращаемый этой функцией, будет использоваться во всем остальном коде.

Часть 1 подробно

В части 1 мы хотим настроить сервер на самом базовом уровне: библиотеки ОС, языки программирования и т. Д.

Если бы мы делали это вручную, мы бы подключились по ssh, а затем запустили бы кучу ap-get install ... из оболочки.

Ну, мы можем сделать то же самое в коде Python, благодаря объекту подключения. Вот код.

def _create_vm(conn):
    _install_packages(conn)
    _install_python(conn)
    _install_venv(conn)


def _install_packages(conn):
    conn.sudo('apt-get -y update')
    conn.sudo('apt-get -y upgrade')
    conn.sudo('apt-get install -y build-essential')
    #conn.sudo('apt-get install -y checkinstall')
    conn.sudo('apt-get install -y libreadline-gplv2-dev')
    conn.sudo('apt-get install -y libncurses-dev')
    conn.sudo('apt-get install -y libncursesw5-dev')
    conn.sudo('apt-get install -y libssl-dev')
    conn.sudo('apt-get install -y libsqlite3-dev')
    conn.sudo('apt-get install -y tk-dev')
    conn.sudo('apt-get install -y libgdbm-dev')
    conn.sudo('apt-get install -y libpq-dev')
    conn.sudo('apt-get install -y libc6-dev')
    conn.sudo('apt-get install -y libbz2-dev')
    conn.sudo('apt-get install -y zlib1g-dev')
    conn.sudo('apt-get install -y openssl')
    conn.sudo('apt-get install -y libffi-dev')
    conn.sudo('apt-get install -y python3-dev')
    conn.sudo('apt-get install -y python3-setuptools')
    conn.sudo('apt-get install -y uuid-dev')
    conn.sudo('apt-get install -y lzma-dev')
    conn.sudo('apt-get install -y wget')
    conn.sudo('apt-get install -y git')
    conn.sudo('apt-get install -y postgresql')


def _install_python(conn):
    """Install python 3.7 in the remote machine."""

    res = conn.run('python3 --version')
    if '3.7' in res.stdout.strip():
        # Python >= 3.7 already exists
        return

    conn.run('rm -rf /tmp/Python3.7 && mkdir /tmp/Python3.7')

    with conn.cd('/tmp/Python3.7'):
        conn.run('wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz')
        conn.run('tar xvf Python-3.7.0.tar.xz')

    with conn.cd ('/tmp/Python3.7/Python-3.7.0'):
        conn.run('./configure --enable-optimizations')
        conn.run('make')

    # see https://github.com/pyinvoke/invoke/issues/459
    conn.sudo('bash -c "cd /tmp/Python3.7/Python-3.7.0 && make altinstall"')


def _install_venv(conn):
    """Install virtualenv, virtualenvwrapper."""

    res = conn.run('which python3.7')
    res = res.stdout.strip()
    py_path = res

    conn.sudo('apt install -y virtualenvwrapper')

    # for a standard Debian distro
    venv_sh = '/usr/share/virtualenvwrapper/virtualenvwrapper.sh'

    conn.run('echo >> ~/.bashrc')  # new line
    conn.run(f'echo source {venv_sh} >> ~/.bashrc')
    conn.run('echo >> ~/.bashrc')  # new line
    conn.run('echo export LC_ALL=en_US.UTF-8 >> ~/.bashrc')
    conn.run('source ~/.bashrc')
    env = environ['VENV_NAME']
    with conn.prefix(f'source {venv_sh}'):
        conn.run(f'mkvirtualenv -p {py_path} {env}')

Приведенный выше код реализует три подэтапа, которые я обсуждал ранее:

  1. Установите все библиотеки , которые нам нужны на уровне ОС (в данном случае есть также git и postgresql-сервер , среди многих других).
  2. Установите конкретную версию Python, которую мы хотим, скомпилировав ее из исходного кода.
  3. Установите virtualenvwrapper . На самом деле вы можете использовать любое управление программным обеспечением virtualenv или нет, если машина предназначена только для одного приложения Python.

Если вы внимательно посмотрите на код, вы поймете шаблон программирования за одну секунду: каждая задача выполняется путем создания объекта подключения и использования его метода .run() с аргументом той же команды, которую вы выполнили бы вручную.

Это еще одна причина, по которой я сказал, что очень важно выполнить ручное развертывание хотя бы один раз!

Часть 2 подробно

В части 2 я сказал, что мы хотим сделать две вещи:

  1. Получите исходный код приложения в машине.
  2. Установите все требования Python.

Давайте посмотрим на первый. В следующей функции я просто использую некоторые команды git , чтобы убедиться, что код извлечен из репозитория в машине.

На первый взгляд эта функция может показаться немного сложной, но это не так. Я написал общую функцию, которая позволяет вам проверять конкретную ветвь по имени или конкретную фиксацию по ее хэшу, и именно поэтому функция выглядит сложной.

Но вы могли бы упростить его и всегда делать что-то вроде git pull origin master , и это все равно будет работать!

def _pull_repo(conn, branch=None, commit=None):
    if branch and commit:
        raise ValueError('Cannot provide both branch name and commit hash')
    source = environ['GIT_DIR']
    if not branch:
        branch = environ['GIT_DEFAULT_BRANCH']
    repo = environ['REPO_URL']
    if commit:
        print('Hash provided. Resetting to that commit.')
        conn.run(
            f"cd {source} && "
            'git stash && '
            f'git reset --hard {commit} && '
            'git checkout -B tmp_branch'
        )
    else:
        if conn.run(f'test -e {source}/.git', warn=True).ok:
            print('Repo already exists.')
        else:
            print('Repo did not exist. Creating it...')
            conn.run(f'git clone {repo} {source}')
            conn.run(f'cd {source} && git remote set-url origin {repo}')
        print('Checking out the requested branch...')
        conn.run(f'cd {source} && git fetch origin && git checkout {branch} && git pull origin {branch}')
    current_hash = conn.run(f'cd {source} && git log -n 1 --format=%H', hide='both')
    current_hash = current_hash.stdout.strip()
    print(f'Checked out {current_hash}')
    return current_hash

Вы, вероятно, заметили, что есть некоторые переменные, загруженные из environ . Я займусь этим через минуту, но в основном причина в том, что вы можете сохранить имя репозитория и учетные данные пользователя в секрете, поэтому лучше не включать их непосредственно в код. В любом случае, потерпите со мной минутку, и эта часть будет ясна.

Вторая часть, установить требования Python намного проще. Единственный трюк здесь в том, что я использую conn.cd() и conn.prefix() для активации virtualenv перед установкой требований. Кроме этого, основная команда точно такая же, как и при запуске вручную: pip install -r requirements.txt

def _install_project(conn):
    repo_path = environ['GIT_DIR']
    venv_name = environ['VENV_NAME']
    venv_sh = 'source /usr/share/virtualenvwrapper/virtualenvwrapper.sh'
    with conn.cd(repo_path):
        with conn.prefix(
            f'{venv_sh} && workon {venv_name}'
        ):
            conn.run('pip install --upgrade pip')
            conn.run('pip install -r requirements.txt')
            # If your project as a `setup.py` then
            # install project.
            #conn.run('pip install -e .')

Часть 3 подробно

Часть 3 самая простая, потому что я использую Gunicorn в качестве рабочего веб-сервера, и для его запуска вам просто нужно запустить простую строку: gunicorn .

def _restart_web(conn):
    try:
        conn.sudo('pkill gunicorn')
    except:
        pass # may not be running at all.
    repo_path = environ['GIT_DIR']
    venv_name = environ['VENV_NAME']
    venv_sh = 'source /usr/share/virtualenvwrapper/virtualenvwrapper.sh'
    with conn.cd(repo_path):
        with conn.prefix(
            f'{venv_sh} && workon {venv_name}'
        ):
            conn.run("gunicorn app:app -b 0.0.0.0:8080 -w 3 --daemon")

ЛАДНО, ладно… Я делаю еще несколько вещей в коде:

  1. Во-первых, я останавливаю процесс gunicorn , если он уже запущен. Это приведет к небольшому простою в приложении.
  2. Затем я использую некоторые аргументы конфигурации для нового процесса gunicorn , чтобы убедиться, что он работает правильно: -b он привязывает его к нужному порту (8080 в этом примере); -w указывает количество рабочих (процессов); --daemon запускает его в фоновом режиме, так что вам не нужно держать соединение открытым.

И это все! Последнее, на что нам нужно обратить внимание, – это как загрузить переменные среды.

Есть масса способов сделать это. В этом проекте я решил создать файл secret.py , который определяет переменные с помощью os.environ [..]=.. , а затем делает импорт секретного из основного файла.

Для доказательства концепций я искал только готовое к использованию приложение Flask. Я хотел использовать приложение, которое не было разработано мной, чтобы показать вам, насколько гибок этот код.

Я нашел пример приложения, опубликованного на GitHub компанией Digital Ocean. Я не имею к ним никакого отношения, но это выглядело хорошо для этого проекта, поэтому вы видите его в secret.py файл. Вот он:

# File secret.py

from os import environ, path

### Connection
environ['REMOTE_HOST'] = '172.104.239.248'
environ['REMOTE_USER'] = '****'
environ['REMOTE_PASSWORD'] = '********+'
#
## Python venv
environ['VENV_NAME'] = 'prod-api'
#
### Git
environ['GIT_DIR'] = '~/app'
environ['GIT_DEFAULT_BRANCH'] = 'main'
environ['REPO_URL'] = 'https://github.com/digitalocean/sample-flask.git'

В главном файле, который вы можете найти в этом gist , я просто делаю import secret , и все переменные среды загружаются. Два файла main.py и secret.py должен находиться в том же каталоге.

Последний трюк!

Код был написан несколько лет назад, но есть один последний трюк, который я реализовал и которым хочу поделиться с вами.

Мотивация заключается в том, что иногда мне захочется запустить одну из функций, которые мы видели, и только одну. И иногда им могут понадобиться аргументы во входных данных (это относится к _pull_repo ).

Это означает, что я хочу иметь точку входа __main__ , которая запускает только функцию, которую я хочу запустить в данный конкретный момент, а также передает ей любой аргумент, поступающий из командной строки.

Вот что я придумал два года назад.

def main(tasks):
    if len(tasks) <= 1:
        print('No task name found')
        return
    i = 1
    while i < len(tasks):
        try:
            fn = getattr(sys.modules[__name__], tasks[i])
        except AttributeError:
            print(f'Cannot find task {tasks[i]}. Quit.')
            return
        params = {}
        j = i + 1
        while j < len(tasks) and '=' in tasks[j]:
            k, v = tasks[j].split('=')
            params[k] = v
            j += 1
        i = j
        print(f'Function is {fn}')
        print(f'args are {params}')
        fn(**params)


if __name__ == '__main__':
    '''
    Run it with
    $ python main  = =  =
    E.g.
    $ python main create_vm
    
    Or
    $ python main pull_repo branch=develop
    '''
    import sys
    tasks = sys.argv
    main(tasks)

Давайте проверим это!

Пришло время проверить. Я хочу заранее сказать вам, что результат хороший: я был очень счастлив, что мой код работал идеально, даже если я не использовал его некоторое время!

Вот что я сделал.

Во-первых, я создал небольшую машину на Linode. Я не связан с ними, я просто хотел использовать другого поставщика, так как код приложения принадлежит Digital Ocean.

Самая маленькая машина на Linode стоит 5 долларов (у других поставщиков такая же цена), и весь тест длился менее 5 минут, поэтому я потратил всего несколько центов. Я выбрал Debian 10 в качестве ОС.

Затем я скопировал хост, имя пользователя и пароль root из консоли Linode в secret.py файл.

Затем я загрузил Github gist, сохранив то же имя файла azPyDeploy.py в том же каталоге, где secret.py есть.

И, наконец, я выполнил четыре простые команды.

python azPyDepl.py create_vm
python azPyDepl.py pull_repo
python azPyDepl.py install_project
python azPyDepl.py restart_web

Первый занял несколько минут (помните, что он компилирует и устанавливает Python), и когда все было сделано, веб-приложение работало на сервере!!

Скриншот с 2021-02-28 16-09-12.png

Это здорово! Приложение Flask, развернутое исключительно через Python. Я должен сказать, что большое спасибо разработчикам Fabric .

Затем я снял сервер (так что IP, который вы видите на рисунке, больше не существует), чтобы не тратить больше $$.

Заключительные комментарии и код

Я знаю, что это не похоже на то, как вы бы сделали развертывание производства сегодня. Даже я все время использую Docker и некоторые более продвинутые инфраструктурные системы, предоставляемые AWS.

Если по какой-то причине вы не можете использовать больше автоматизированных систем, то проект в этом руководстве может быть полезен.

Но в целом, главным преимуществом этого кода для меня является ценность процесса обучения . Понимание серверов и того, как все работает за кулисами, было очень важным в моей профессиональной карьере, и я надеюсь, что этот пост прольет некоторый свет и поможет вам тоже!

Вот полный код, на случай, если gist не сработает.

Дайте мне знать, если вы столкнулись с какой-либо проблемой при воспроизведении этого урока.

# This script needs
# $ pip install fabric

from os import environ

from fabric import Connection

# Create a file `secret.py` in the same directory as this one
# and add in it the credentials to connect to the server and GitHub.
# Here is a template.
#############
# File secret.py

#from os import environ, path
#
### Connection
#environ['REMOTE_HOST'] = '172.104.239.248'
#environ['REMOTE_USER'] = '****'
#environ['REMOTE_PASSWORD'] = '********'
#
## Python venv
#environ['VENV_NAME'] = 'prod-api'
#
### Git
#environ['GIT_DIR'] = '~/app'
#environ['GIT_DEFAULT_BRANCH'] = 'main'
#environ['REPO_URL'] = 'https://github.com/digitalocean/sample-flask.git'
#############
import secret


def create_conn():
    # Switch the two lines if you connect via PEM Key
    # instead of password.
    params = {
        #'key_filename': environ['SSH_KEY_PATH']}
        'password': environ['REMOTE_PASSWORD']
    }
    conn = Connection(
        host=environ['REMOTE_HOST'],
        user=environ['REMOTE_USER'],
        connect_kwargs=params,
    )
    return conn


######################
# Internal Functions #
######################


def _create_vm(conn):
    _install_packages(conn)
    _install_python(conn)
    _install_venv(conn)


def _install_packages(conn):
    conn.sudo('apt-get -y update')
    conn.sudo('apt-get -y upgrade')
    conn.sudo('apt-get install -y build-essential')
    #conn.sudo('apt-get install -y checkinstall')
    conn.sudo('apt-get install -y libreadline-gplv2-dev')
    conn.sudo('apt-get install -y libncurses-dev')
    conn.sudo('apt-get install -y libncursesw5-dev')
    conn.sudo('apt-get install -y libssl-dev')
    conn.sudo('apt-get install -y libsqlite3-dev')
    conn.sudo('apt-get install -y tk-dev')
    conn.sudo('apt-get install -y libgdbm-dev')
    conn.sudo('apt-get install -y libpq-dev')
    conn.sudo('apt-get install -y libc6-dev')
    conn.sudo('apt-get install -y libbz2-dev')
    conn.sudo('apt-get install -y zlib1g-dev')
    conn.sudo('apt-get install -y openssl')
    conn.sudo('apt-get install -y libffi-dev')
    conn.sudo('apt-get install -y python3-dev')
    conn.sudo('apt-get install -y python3-setuptools')
    conn.sudo('apt-get install -y uuid-dev')
    conn.sudo('apt-get install -y lzma-dev')
    conn.sudo('apt-get install -y wget')
    conn.sudo('apt-get install -y git')
    conn.sudo('apt-get install -y postgresql')


def _install_python(conn):
    """Install python 3.7 in the remote machine."""

    res = conn.run('python3 --version')
    if '3.7' in res.stdout.strip():
        # Python >= 3.7 already exists
        return

    conn.run('rm -rf /tmp/Python3.7 && mkdir /tmp/Python3.7')

    with conn.cd('/tmp/Python3.7'):
        conn.run('wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz')
        conn.run('tar xvf Python-3.7.0.tar.xz')

    with conn.cd ('/tmp/Python3.7/Python-3.7.0'):
        conn.run('./configure --enable-optimizations')
        conn.run('make')

    # see https://github.com/pyinvoke/invoke/issues/459
    conn.sudo('bash -c "cd /tmp/Python3.7/Python-3.7.0 && make altinstall"')


def _install_venv(conn):
    """Install virtualenv, virtualenvwrapper."""

    res = conn.run('which python3.7')
    res = res.stdout.strip()
    py_path = res

    conn.sudo('apt install -y virtualenvwrapper')

    # for a standard Debian distro
    venv_sh = '/usr/share/virtualenvwrapper/virtualenvwrapper.sh'

    conn.run('echo >> ~/.bashrc')  # new line
    conn.run(f'echo source {venv_sh} >> ~/.bashrc')
    conn.run('echo >> ~/.bashrc')  # new line
    conn.run('echo export LC_ALL=en_US.UTF-8 >> ~/.bashrc')
    conn.run('source ~/.bashrc')
    env = environ['VENV_NAME']
    with conn.prefix(f'source {venv_sh}'):
        conn.run(f'mkvirtualenv -p {py_path} {env}')


def _pull_repo(conn, branch=None, commit=None):
    if branch and commit:
        raise ValueError('Cannot provide both branch name and commit hash')
    source = environ['GIT_DIR']
    if not branch:
        branch = environ['GIT_DEFAULT_BRANCH']
    repo = environ['REPO_URL']
    if commit:
        print('Hash provided. Resetting to that commit.')
        conn.run(
            f"cd {source} && "
            'git stash && '
            f'git reset --hard {commit} && '
            'git checkout -B tmp_branch'
        )
    else:
        if conn.run(f'test -e {source}/.git', warn=True).ok:
            print('Repo already exists.')
            # run("cd %s && git pull upstream %s" % (source_dir, branch))
            #conn.run(f'cd {source} && git fetch origin {branch}')
            #conn.run(f'cd {source} && git reset --hard origin/{branch}')
        else:
            print('Repo did not exist. Creating it...')
            conn.run(f'git clone {repo} {source}')
            conn.run(f'cd {source} && git remote set-url origin {repo}')
        print('Checking out the requested branch...')
        conn.run(f'cd {source} && git fetch origin && git checkout {branch} && git pull origin {branch}')
    current_hash = conn.run(f'cd {source} && git log -n 1 --format=%H', hide='both')
    current_hash = current_hash.stdout.strip()
    print(f'Checked out {current_hash}')
    return current_hash


def _install_project(conn):
    repo_path = environ['GIT_DIR']
    venv_name = environ['VENV_NAME']
    venv_sh = 'source /usr/share/virtualenvwrapper/virtualenvwrapper.sh'
    with conn.cd(repo_path):
        with conn.prefix(
            f'{venv_sh} && workon {venv_name}'
        ):
            conn.run('pip install --upgrade pip')
            conn.run('pip install -r requirements.txt')
            # If your project as a `setup.py` then
            # install project.
            #conn.run('pip install -e .')


def _restart_web(conn):
    try:
        conn.sudo('pkill gunicorn')
    except:
        pass # may not be running at all.
    repo_path = environ['GIT_DIR']
    venv_name = environ['VENV_NAME']
    venv_sh = 'source /usr/share/virtualenvwrapper/virtualenvwrapper.sh'
    with conn.cd(repo_path):
        with conn.prefix(
            f'{venv_sh} && workon {venv_name}'
        ):
            conn.run("gunicorn app:app -b 0.0.0.0:8080 -w 3 --daemon")


#####################################
# Functions used from the __main__ ##
#####################################


def create_vm(**kwargs):
    _create_vm(create_conn())


def pull_repo(**kwargs):
    conn = create_conn()
    _pull_repo(conn, **kwargs)


def install_project(**kwargs):
    _install_project(create_conn())


def restart_web(**kwargs):
    _restart_web(create_conn())


def main(tasks):
    if len(tasks) <= 1:
        print('No task name found')
        return
    i = 1
    while i < len(tasks):
        try:
            fn = getattr(sys.modules[__name__], tasks[i])
        except AttributeError:
            print(f'Cannot find task {tasks[i]}. Quit.')
            return
        params = {}
        j = i + 1
        while j < len(tasks) and '=' in tasks[j]:
            k, v = tasks[j].split('=')
            params[k] = v
            j += 1
        i = j
        print(f'Function is {fn}')
        print(f'args are {params}')
        fn(**params)


if __name__ == '__main__':
    '''
    Run it with
    >>python main  = =  =
    E.g.
    $ python main create_vm
    '''
    import sys
    tasks = sys.argv
    main(tasks)