Usando GPU en Proxmox

Quizá todavía no lo conceis, pero Proxmox es el software definitivo si queréis montar vuestra propia infraestructura en casa (o en vuestra empresa), es un hypervisor que os permite tener vuestra propia «nube» creando máquinas virtuales, contenedores y gestionando almacenamiento, backups y alta disponibilidad.

Yo llevo unos meses con esto, desde que me compré y quise dar uso, unas placas chinas para aprovechar los Xeon de segunda mano que ahora se encuentran tan baratos y, la verdad, es como tener un AWS particular (salvando muuuuchas diferencias). El caso es que lo único que me quedaba por probar era cómo tener una máquina virtual controlada por proxmox que me permitiese hacer AI… Pero para eso necesitaba usar una GPU y esto no es taaan sencillo. Así que partamos de un servidor que tiene una tarjeta gráfica (en mi caso una RTX 3070) y veamos cómo configurar el ordenador para meterlo en un cluster proxmox estando preparado para tener VMs que usen esa GPU.

¿cual es el problema realmente?

El problema es que un hypervisor lo que hace es ejecutar máquinas virtuales a las que ha asignado cierta parte de sus recursos (disco, memoria, etc) y permitir el uso compartido de todo lo que se puede compartir. Por desgracia la GPU no se puede compartir de la misma manera que una CPU (hay algunos modelos que tienen una tecnología que se llama VGPU que parece que si permitirán hacerlo, pero por ahora las que tengo yo no). Es por eso que lo que se hace es pasarle a la máquina virtual todo el bus PCI en cuestión para que lo gestione de manera independiente. Para que esto se pueda llevar a cabo es importante que el SO de proxmox no esté usando este bus para nada (que no tenga los drives instalados siquiera). El servidor que yo he usado tenía video integrado y configuré la bios para que usase ese como video primario (y así instalé proxmox sin utilizar la tarjeta gráfica). Pasos importantes con la BIOS:

  • Activar la tarjeta integrada (si la tiene)
  • Activar todos los modos de multihilo VT-d y cualquier referencia a IOMMU

Dejo un enlace que lo explica para varias placas base.

Lo siguiente, desde el servidor con ya proxmox instalado será editar el arranque de grub poniendo lo siguiente en el archivo /etc/default/grub:

GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt"

Y ejecuta después update-grub, tras lo cual tendrás que reiniciar la máquina.

Para comprobar que está todo activo ejecuta este comandos:

dmesg | grep -e DMAR -e IOMMU

El resultado tendría que ser algo como esto:

[    0.008366] ACPI: DMAR 0x000000007A29ED38 0000A8 (v01 INTEL  EDK2     00000002      01000013)
[    0.008390] ACPI: Reserving DMAR table memory at [mem 0x7a29ed38-0x7a29eddf]
[    0.098662] DMAR: IOMMU enabled
[    0.255710] DMAR: Host address width 39
[    0.255711] DMAR: DRHD base: 0x000000fed90000 flags: 0x0
[    0.255721] DMAR: dmar0: reg_base_addr fed90000 ver 1:0 cap 1c0000c40660462 ecap 19e2ff0505e
[    0.255723] DMAR: DRHD base: 0x000000fed91000 flags: 0x1
[    0.255727] DMAR: dmar1: reg_base_addr fed91000 ver 1:0 cap d2008c40660462 ecap f050da
[    0.255728] DMAR: RMRR base: 0x00000079d2f000 end: 0x00000079d4efff
[    0.255731] DMAR: RMRR base: 0x0000007b800000 end: 0x0000007fffffff
[    0.255733] DMAR-IR: IOAPIC id 2 under DRHD base  0xfed91000 IOMMU 1
[    0.255734] DMAR-IR: HPET id 0 under DRHD base 0xfed91000
[    0.255735] DMAR-IR: Queued invalidation will be enabled to support x2apic and Intr-remapping.
[    0.257485] DMAR-IR: Enabled IRQ remapping in x2apic mode
[    0.600499] DMAR: No ATSR found
[    0.600500] DMAR: No SATC found
[    0.600501] DMAR: IOMMU feature fl1gp_support inconsistent
[    0.600502] DMAR: IOMMU feature pgsel_inv inconsistent
[    0.600503] DMAR: IOMMU feature nwfs inconsistent
[    0.600504] DMAR: IOMMU feature pasid inconsistent
[    0.600505] DMAR: IOMMU feature eafs inconsistent
[    0.600506] DMAR: IOMMU feature prs inconsistent
[    0.600507] DMAR: IOMMU feature nest inconsistent
[    0.600508] DMAR: IOMMU feature mts inconsistent
[    0.600509] DMAR: IOMMU feature sc_support inconsistent
[    0.600509] DMAR: IOMMU feature dev_iotlb_support inconsistent
[    0.600510] DMAR: dmar0: Using Queued invalidation
[    0.600513] DMAR: dmar1: Using Queued invalidation
[    0.600990] DMAR: Intel(R) Virtualization Technology for Directed I/O

Donde lo relevante es el IOMMU enabled y Enabled IRQ remmaping. Si todo está ok podemos ver os grupos iommu con este comando:

pvesh get /nodes/pascal/hardware/pci --pci-class-blacklist ""

Que nos debería dar una salida como la siguiente:

??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
? class    ? device ? id           ? iommugroup ? vendor ? device_name                                                                             ? mdev ? su
??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
...
?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
? 0x030000 ? 0x2484 ? 0000:01:00.0 ?          2 ? 0x10de ? GA104 [GeForce RTX 3070]                                                                ?      ? 0x
??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????

Ahora nos falta asegurarnos de que proxmox no va a utilizar esta gpu y estaríamos casi listos para crear nuestra vm:

echo "options vfio_iommu_type1 allow_unsafe_interrupts=1" > /etc/modprobe.d/iommu_unsafe_interrupts.conf
echo "vfio" >> /etc/modules
echo "vfio_iommu_type1" >> /etc/modules
echo "vfio_pci" >> /etc/modules
update-initramfs -u -k all
systemctl reboot

Comprobaremos que se carga vfio y pondremos en lista negra los drivers de nuestra gpu

dmesg | grep -i vfio
echo "options kvm ignore_msrs=1 report_ignored_msrs=0" > /etc/modprobe.d/kvm.conf
lspci -nn | grep 'NVIDIA'

Veremos los ids de nuestro dispositivo

01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA104 [GeForce RTX 3070] [10de:2484] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation GA104 High Definition Audio Controller [10de:228b] (rev a1)

Y los usaremos para ponerlos en lista negra para los drivers posibles:

echo "options vfio-pci ids=10de:2484,10de:228b" >> /etc/modprobe.d/vfio.conf
echo "blacklist nouveau" >> /etc/modprobe.d/blacklist.conf
echo "blacklist nvidia" >> /etc/modprobe.d/blacklist.conf
echo "blacklist nvidiafb" >> /etc/modprobe.d/blacklist.conf
echo "blacklist nvidia_drm" >> /etc/modprobe.d/blacklist.conf
systemctl reboot

Y con esto ya está listo nuestro proxmox para compartir el PCI… Os recomiendo que si vais a unirlo a un cluster lo hagáis ahora, luego si creais una vm os va a ser más complicado. En cualquier caso, lo que queda es crear una máquina virtual y añadirle el pci de la tarjeta.

Para ello simplement creamos una máquina virtual, en mi caso digo que voy a instalar un linux y antes de arrancarla vamos al apartado de hardware y añadimos estos PCI:

Una vez arrancada la máquina e instalado el sistema operativo podemos comprobar si tenemos los drivers de nvida configurados ejecutando nvidia-smi

Así que ya tenemos una máquina con GPU para poder ejecutar nuestros trabajos de AI. Para ello podéis seguir estas instrucciones para instalar ollama o stable difussion en esta máquina virtual… Con la ventaja que aporta tenerlo controlado por Proxmox para hacer backups arrancarlo o pararlo a voluntad, monitorizarlo, etc.

AMAZON SES y cómo enviar correos desde un servidor ubuntu

Enviar correo desde una máquina virtual en Amazon siempre ha sido un castigo. Las limitaciones al puerto 25 y a los controles de tráfico de Amazon hacían poco recomendable poner un servidor de correo «normal» en la infraestructura. Sin embargo – y pagando, claro está – Amazon ha puesto a disposición de todo el mundo un servicio para poder enviar correos sin demasiada complicación (aunque, como veremos, también tiene sus limitaciones).

Lo primero es lo primero, si quieres mandar correos usando Amazon SES. La información general la puedes ver aquí: https://aws.amazon.com/es/ses/ y create una identidad verificada (tendrás que cambiar cosas en el dns para que puedas enviar correo desde cuentas de tu dominio. Lo siguiente será crear una configuración de SMTP para tu cuenta, eso te dará un servidor, usuario y contraseña que usar para mandar correos (y los puertos correspondientes)… Anotalos muy bien que será lo que vamos a utilizar.

Al principio tendrás unas limitaciones muy importantes (para probar no nos afectan demasiado) y tendrás que crear direcciones de correo validada, hazlo y prueba que puedes enviar correos a esas cuentas antes de continuar. Los pasos para poder enviar correo desde un servidor ubuntu serían los siguientes:

  1. Instala postfix
sudo apt install -y postfix libsasl2-modules
  1. Añade estas líneas a /etc/postfix/sasl_passwd
smtp_tls_note_starttls_offer = yes 
smtp_tls_security_level = encrypt 
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd 
smtp_sasl_security_options = noanonymous 
relayhost = [email-smtp.xx-xxxx-xx.amazonaws.com]:587 
smtp_sasl_auth_enable = yes 
smtp_use_tls = yes 
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt 
mydestination = 
  1. En /etc/postfix/sasl_passwd
[email-smtp.xx-xxxx-x.amazonaws.com]:587 USUARIO:PASSWORD
  1. Lanza las modificaciones
sudo newaliases 
sudo postmap hash:/etc/postfix/sasl_passwd 
sudo systemctl restart postfix

Y ya estaría, ya puedes enviar correos desde cuentas de tu dominio con sendmail. Si ves algún problema siempre puedes consultar el log en /var/log/mail.log

Si todo va bien lo siguiente es pedir a Amazon que, por favor, os pongan en producción el sistema para poder enviar correos a todo el mundo. Y ya si eso, en otro post, os cuento como configurar una imagen docker para que lo use…

¿Qué hacer cuando Autofirma no te muestra ningún certificado?

En mi día a día, y en el de cada vez más personas, el uso de la firma digital se ha convertido en algo habitual. No solo es la obligación legal de las administraciones al relacionarnos con ellas, sino que también abre un nuevo abanico de posibilidades para usos particulares de lo más variopinto.

Una de las cosas que la administración lleva haciendo, digamos, bien, estos años es proporcionarnos herramientas para que nuestras penas burocráticas sean un poco menores (ojo, esto que digo es muuuuuy discutible), o al menos debería serlo. Para mi, que uso Linux en mi día a día y que solo arranco Windows por obligación, tener una manera de hacer firma digital era fundamental. Y resulta que la administración creó una aplicación en Java que podemos utilizar también los de LInux ¡Bien!

La herramienta se llama AutoFirma y podéis descargarla del enlace que os he pasado antes. No es que sea una maravilla de la técnica, pero puedes usarla de manera bastante sencilla ya que te coge los certificados del almacén de Firefox… O lo hacía.

El caso es que, de un tiempo a esta parte el diálogo para escoger el certificado me aparecía vacío y tenía que cargar el archivo .p12 donde tengo la copia de seguridad del certificado que quería utilizar… Y eso era muy penoso dada la cantidad de archivos que tengo habitualmente. La verdad es que me sorprendió que dejase de funcionar, pero creí que sería algo pasajero… Pero no lo fue.

Si a vosotros os pasa algo similar, os doy la receta para que vuelva a funcionar:

Revisad los perfiles de firefox

Si tenéis más de un perfil en firefox (en mi caso tenía tres porque instalé versiones beta de Firefox 100) AutoFirma se confunde y no coge el correcto… Podéis ver los perfiles que tenéis escribiendo about:profiles en Firefox.

En mi caso, y dado que no usaba los otros perfiles para nada, me bastó ir al directorio .mozilla/firefox de mi máquina y eliminar los directorios que no eran mi perfil principal y luego editar el archivo profiles.ini para eliminar todo rastro de esos perfiles.

Una vez hecho esto (no es necesario reiniciar Firefox si no queréis) ya tendréis disponibles de nuevo los certificados para hacer la firma…

Gestión de versiones de Flutter

Flutter es un framework que me está gustando bastante, es muy consistente en cuanto a las distintas versiones, se ve muy similar en todas las plataformas y Dart como lenguaje es bastante interesante y nada esotérico (por eso de que usa cosas que ya manejamos y no se dedica a reinventar la rueda).

Flutter Version Management

Tanto es así que empezamos hace unos meses un proyecto de aplicación móvil con Flutter, coincidiendo con la llegada de la versión 2.0 y las cosas empezaron a ir «demasiado deprisa». La comunidad y los mantenedores del sdk parece que se han puesto las pilas y han decidido incluir mejoras a un ritmo trepidante, cuando escribo estas líneas ya vamos por la versión 2.8.1.

Nosotros estabilizamos la app en un contenedor docker usando la versión 2.5.2 de Flutter y nos encontramos ahora que no hay manera de instalar desde cero esa versión con los sistemas que proporciona Flutter. Todos aquellos sistemas en los que hicimos un upgrade ya no los podemos utilizar para desarrollo porque, entre otras cosas, han cambiado librerías y ya no son compatibles algunas partes de nuestro código.

Pero no soy el único con ese problema. Varios de los desarrolladores de Flutter se han encontrado con la misma situación y, afortunadamente, han desarrollado una manera de poder disponer de una versión propia de flutter para cada proyecto… Mediante FVM.

Os cuento los pasos básicos para tener FVM funcionando (partiendo de que has instalado flutter en tu sistema) y cómo usarlo para cada proyecto:

1. Instalar FVM

dart pub global activate fvm

Esto te instala el paquete, pero tendrás que cambiar el entorno para poder acceder al comando fvm cada vez, en mi caso es algo como:

export PATH="$PATH":"$HOME/.pub-cache/bin"

Añadiendo esta línea al final del ~/.bashrc conseguiréis meter en el path el comando FVM

2. Instalar una versión de flutter para su uso posterior

En mi caso quería instalar la versión 2.5.2 y tuve que ejecutar esto:

fvm install 2.5.2

Puedes instalar tantas como quieras (o necesites), puedes conseguir la lista de todas las instaladas con el comando fvm list

3. Usar una versión concreta en tu proyecto

Dentro del directorio del proyecto en el que quieras usar esta versión puedes sustituir el uso del comando flutter por el comando fvm flutter que te ejecutará la versión correspondiente, para indicar qué versión quieres usar debes escribir:

fvm use 2.5.2

Esto te genera un directorio .fvm en donde se almacenarán los enlaces correspondientes, no olvides incluir en tu .gitignore el directorio .fvm/flutter_sdk

Con esto y algunas cosillas más (os dejo consultar la documentación) ya podréis desarrollar y depurar con la versión de flutter que queráis.

Subir automáticamente tu app a la play store

Últimamente estoy muy metido en esto del CI/CD (Integración continúa/Despliegue continuo) que no significa, ni más ni menos, que las actualizaciones a los repositorios de código generan automáticamente todos los artefactos necesarios para poder comprobar que están correctos y que pueden desplegarse en producción, e incluso desplegarlos si así lo consideramos.

En el caso de servicios en internet de cualquier tipo (webs, APIs, etc.) el método de despliegue suele ser más sencillo de automatizar (básicamente generas un contenedor con una versión nueva y lo «empujas» al sistema de producción), más sencillo incluso si utilizamos kubernetes o ansible. Pero en el caso de distribución de aplicaciones a instalar en clientes es más complicado. En el caso que traemos aquí lo que queremos hacer es subir a la tienda de google play (y ya veremos qué hacemos con apple) una nueva versión de una aplicación móvil.

En principio debería ser sencillo, ya que solo tenemos que utilizar el API de Google Play para desarrolladores. Pero antes, tenemos que conseguir ciertas «autorizaciones» de google que no son tan triviales como deberían. Para haceros la tarea un poco más sencilla, aquí os dejo un script python que hace justo lo que queremos: subir a la tienda nuestro apk recién compilado al canal alpha (upload_apks.py)

"""Uploads an apk to the alpha track."""

import argparse

from googleapiclient import discovery
import httplib2
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client import client

TRACK = 'alpha'  # Can be 'alpha', beta', 'production' or 'rollout'
SERVICE_ACCOUNT_EMAIL = (
    'ENTER_YOUR_SERVICE_ACCOUNT_EMAIL_HERE@developer.gserviceaccount.com')

# Declare command-line flags.
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('package_name',
                       help='The package name. Example: com.android.sample')
argparser.add_argument('apk_file',
                       nargs='?',
                       default='test.apk',
                       help='The path to the APK file to upload.')
argparser.add_argument('key_file',
                       nargs='?',
                       default='key.json',
                       help='key in json format for service account')
argparser.add_argument('version',
                       nargs='?',
                       default='New version',
                       help='Version name')

def main():
  # Process flags and read their values.
  flags = argparser.parse_args()

  package_name = flags.package_name
  apk_file = flags.apk_file
  key_file = flags.key_file
  version = flags.version
  
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
      key_file,
      scopes='https://www.googleapis.com/auth/androidpublisher')
  http = httplib2.Http()
  http = credentials.authorize(http)

  service = discovery.build('androidpublisher', 'v3', http=http)

  try:
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    apk_response = service.edits().apks().upload(
        editId=edit_id,
        packageName=package_name,
        media_body=apk_file).execute()

    print ('Version code {} has been uploaded'.format(apk_response['versionCode']))

    track_response = service.edits().tracks().update(
        editId=edit_id,
        track=TRACK,
        packageName=package_name,
        body={u'releases': [{
            u'name': version,
            u'versionCodes': [apk_response['versionCode']],
            u'status': u'completed',
        }]}).execute()

    print ('Track {} is set for release(s) {}' .format (
        track_response['track'], str(track_response['releases'])))

    commit_request = service.edits().commit(
        editId=edit_id, packageName=package_name).execute()

    print ('Edit "{}" has been committed'.format(commit_request['id']))

  except client.AccessTokenRefreshError:
    print ('The credentials have been revoked or expired, please re-run the '
           'application to re-authorize')

if __name__ == '__main__':
  main()

Antes de poder ejecutar este código deberás instalar algunas librerías (o ponerlas en el Dockerfile)

apt-get update -y
apt-get install python3 -y
apt-get install python3-pip -y
pip3 install google-api-python-client
pip3 install --upgrade oauth2client

Y después crear un archivo pc-api.json con este contenido (que luego os explico como conseguir):

{
  "type": "service_account",
  "project_id": "<project-id>",
  "private_key_id": "<private-key-id>",
  "private_key": "-----BEGIN PRIVATE KEY-----...\n-----END PRIVATE KEY-----\n",
  "client_email": "<service-account>",
  "client_id": "<client-id>",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/<service-account>"
}

Y ya, por último, solo nos quedaría ejecutar el comando con estos parámetros:

upload_apks.py com.paquete.app apk/app-release.apk res/pc-api.json ${VERSION}

Y ahora la parte más pesada… Los datos a rellenar en pc-api.json, para ello deberás configurar un proyecto API en la consola cloud de Google y crear una cuenta de servicio… Este no es un proceso inmediato, pero hay buenas instrucciones en la página de google.

Luego hay que ir a la sección de la Google Play Console al apartado Acceso a API, donde deberás vincular el proyecto que acabas de crear y crear las cuentas de servicio necesarias.

Para después en la sección de Usuarios y permisos dar permisos a la cuenta de servicio que acabamos de crear, al menos estos:

El tema de extraer la clave privada y eso ya os lo dejo para después por si alguien tiene curiosidad…

ACTUALIZACIÓN 4/01/2022

Como ahora Google solo deja subir bundles (.aab) dejo aquí el código modificado para poder hacerlo (según el tipo de archivo que elijamos):

#!/usr/bin/env python3
#
# Copyright 2014 Marta Rodriguez.
# Modified by Jose Antonio Espinosa (2021-2022)
#
# Licensed under the Apache License, Version 2.0 (the 'License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Uploads an apk/aab to the alpha track."""

import argparse

from googleapiclient import discovery
import httplib2
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client import client
import os.path
import mimetypes

mimetypes.add_type("application/octet-stream", ".apk")
mimetypes.add_type("application/octet-stream", ".aab")

TRACK = 'alpha'  # Can be 'alpha', beta', 'production' or 'rollout'
SERVICE_ACCOUNT_EMAIL = (
    'ENTER_YOUR_SERVICE_ACCOUNT_EMAIL_HERE@developer.gserviceaccount.com')

# Declare command-line flags.
argparser = argparse.ArgumentParser(add_help=False)
argparser.add_argument('package_name',
                       help='The package name. Example: com.android.sample')
argparser.add_argument('apk_file',
                       nargs='?',
                       default='test.apk',
                       help='The path to the APK file to upload.')
argparser.add_argument('key_file',
                       nargs='?',
                       default='key.json',
                       help='key in json format for service account')
argparser.add_argument('version',
                       nargs='?',
                       default='New version',
                       help='Version name')

def main():
  # Process flags and read their values.
  flags = argparser.parse_args()

  package_name = flags.package_name
  apk_file = flags.apk_file
  key_file = flags.key_file
  version = flags.version
  extension = os.path.splitext(apk_file)[1]
  
  # Create an httplib2.Http object to handle our HTTP requests and authorize it
  # with the Credentials. Note that the first parameter, service_account_name,
  # is the Email address created for the Service account. It must be the email
  # address associated with the key that was created.
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
      key_file,
      scopes='https://www.googleapis.com/auth/androidpublisher')
  http = httplib2.Http()
  http = credentials.authorize(http)

  service = discovery.build('androidpublisher', 'v3', http=http)

  try:
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    if extension == '.apk':
        apk_response = service.edits().apks().upload(
            editId=edit_id,
            packageName=package_name,
            media_body=apk_file).execute()
    else:
        apk_response = service.edits().bundles().upload(
            editId=edit_id,
            packageName=package_name,
            media_body=apk_file).execute()

    print ('Version code {} has been uploaded'.format(apk_response['versionCode']))

    track_response = service.edits().tracks().update(
        editId=edit_id,
        track=TRACK,
        packageName=package_name,
        body={u'releases': [{
            u'name': version,
            u'versionCodes': [apk_response['versionCode']],
            u'status': u'completed',
        }]}).execute()

    print ('Track {} is set for release(s) {}' .format (
        track_response['track'], str(track_response['releases'])))

    commit_request = service.edits().commit(
        editId=edit_id, packageName=package_name).execute()

    print ('Edit "{}" has been committed'.format(commit_request['id']))

  except client.AccessTokenRefreshError:
    print ('The credentials have been revoked or expired, please re-run the '
           'application to re-authorize')

if __name__ == '__main__':
  main()