viernes, mayo 16, 2025

Firebase Studio y Cloudflare

Es importante usar la IA para avanzar rápido y la nueva herramienta de google Firebase Studio es tentadora. Tiene un modo de prototipo que promete generar una aplicación en minutos.

Sería tonto no probarla, ¿no? ¿NO?

Me encuentro escribiendo cosas para el prototipo, ¿las pongo en inglés? ¿entenderá español? ¿qué puedo generar?

And app with user registration and new users are redirected to a subscription page to select between three plans 'basic', 'pro' and 'enterprise'. Each plan has discount if payed annually (10%). The app uses Stripe to handle payments in the backend. After the user has a valid subscription, the user can visit the homepage where he can create a todo list.

Si, con todo y el error And que puse al inicio... vamos a ver qué genera... 


Increíble, la verdad que es sorprendente lo que puedes hacer en minutos con ésta herramienta.  Obvio tiene muchas limitaciones que estoy esquivando, voy a listar algunas.

  • En modo prototipo, no soporta Angular
Ya con esa limitante, para mi, no vale la pena explorar más hasta que google soporte en su herramienta su propio framework.

Después Firebase Studio te ayuda a publicar la aplicación usando Firebase Hosting, es trivial.  ¿Pero qué pasa si quieres usar Cloudflare? ¿Cómo publicas en Cloudflare?  Bueno de eso quería escribir por que para mi no fue trivial.

En éste artículo sólo voy a compartir como publicar en Clouflare Pages, por que también lo puedes publicar usando Cloudflare Workers, pero yo sólo quiero el contenido estático nada de SSR.

La aplicación generada por Firebase Studio es una Nextjs, 😒 He estado esquivando React por años, incluso incursionando con Vue y Svelte que son maravillosos, pero React no me gusta, yo quiero que mis archivos sean html o js o ts o css o scss o json.

Tengo cero experiencia con Nextjs, y la IA no me sirve igual cuando no sé lo que está haciendo.  Pero eh aquí el resultado:

La configuración en next.config.ts:


import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  output: "export", // Use 'export' for static export
  distDir: "out", // Output directory for the static export
  typescript: {
    ignoreBuildErrors: true,
  },
  eslint: {
    ignoreDuringBuilds: true,
  },
  images: {
    unoptimized: true, // Disable Next.js image optimization
    remotePatterns: [
      {
        protocol: "https",
        hostname: "placehold.co",
        port: "",
        pathname: "/**",
      },
    ],
  },
};

export default nextConfig;

Puntos importantes:

Las imágenes guardadas en /public no son accesibles si usas Image tag, para que sirva tienes que deshabilitar la optimización que hace Nextjs con `images.unoptimezed: true`.  output y distDir son opciones que fui encontrando en la web y no estoy seguro de que distDir sea necesario, pero por ahora funciona y la dejé así.

Bien ahora cada vez que corras npm run build creará un directorio out con el sitio estático compilado y listo para publicar.

Ahora Cloudflare.  Crea un nuevo Page dentro de Cloudflare Compute, yo elegí Use direct upload, pero creo que con Import an existing Git repository lo puedes lograr, no lo probé.

Le pones un nombre, todo minúsculas sin espacios y luego te aparece una opción para subir contenido.  En éste punto puedes subir el contenido de tu carpeta out. Yo me regresé a la lista de Workers & Pages, y confirmé de que existía mi nuevo proyecto.


Y aunque ahí dice assets uploaded, no es cierto no subí nada, lo quiero hacer desde mi consola de trabajo usando wrangler.

Después de validar mi cuenta en la terminal puedes correr éste commando en la raíz del proyecto. 'super-app' es el nombre que le pusimos.

$ npx wrangler pages download config super-app
Need to install the following packages:
wrangler@4.15.2
Ok to proceed? (y)


 ⛅️ wrangler 4.15.2
-------------------

Bajará un archivo wrangler.toml básico que necesita un retoquito.

# Generated by Wrangler on Fri May 16 2025 10:21:40 GMT-0600 (Central Standard Time)
name = "super-app"
compatibility_date = "2025-05-16"
pages_build_output_dir = "./out"

[env]
production = { }

Y ahora está listo para publicar...

npx wrangler pages deploy --branch main

--branch main le dice a Cloudflare que debe de ir hasta arriba en el nombre de dominio, de lo contrario usa el nombre del branch que tengas activo en el repositorio git del proyecto.

Para facilitar el ciclo de desarrollo, le agrego a package.json un script:


{
  "name": "nextn",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack -p 9002",
    "genkit:dev": "genkit start -- tsx src/ai/dev.ts",
    "genkit:watch": "genkit start -- tsx --watch src/ai/dev.ts",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "deploy": "next build && npx wrangler pages deploy --branch main",
    "typecheck": "tsc --noEmit"
  },
...

Entonces puedes simplemente ejecutar npm run deploy y te lo compila y te lo publica.

viernes, mayo 02, 2025

Angular, proxy y CORS

La eterna batalla contra CORS: Un recordatorio para mi yo del futuro

De vez en cuando, sobre todo en nuevos proyectos, me tropiezo con la misma piedra. Y hoy, después de perder medio día entre búsquedas en Google, posts antiguos en Stack Overflow, y consultas a Perplexity, ChatGPT 4o-mini, Claude y Deepseek, he decidido documentar la solución para Juan Pablo del futuro.

El problema recurrente

Usualmente, en mis proyectos con Angular (v18), configuro mis variables de ambiente en environment.ts y environment.prod.ts. Típicamente como algo así:

export const environment = {
  production: false,
  protocol: 'http',
  baseUrl: 'http://localhost:5000/',
};

Y luego tengo un servicio que puede hacer una petición al backend así:

  // Use baseUrl from environment and append the specific API path segment
  private apiUrl = environment.baseUrl + 'api/v1'; // Ensure this points to your Go backend

  // Fetch subscription status from API and update the BehaviorSubject
  loadSubscriptionStatus(): Observable<SubscriptionStatus> {
    const headers = this.getAuthHeaders();
    return this.http.get<SubscriptionStatus>(`${this.apiUrl}/subscriptions/current`, { headers })
      .pipe(
        tap(status => this.subscriptionStatusSubject.next(status)), // Update state on success
        catchError(err => {
          this.subscriptionStatusSubject.next(null); // Clear state on error
          return this.handleError(err); // Propagate error
        })
      );
  }

El dolor de cabeza familiar

Y por supuesto, la primera vez el navegador me responde con un error:

Access to XMLHttpRequest at 'http://localhost:5000/api/v1/subscriptions/current' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

Me duele la cabeza cada vez que veo ese error... y recuerdo: "¡Ah, tengo que configurar un proxy!". Mientras desarrollo es buena idea, ya en producción no será necesario. Y entonces busco un proyecto anterior y copio el archivo proxy.conf.json y lo acomodo en el angular.json para que se lea cada vez que corro ng serve.

{
    "/api": {
        "target": "http://localhost:5000",
        "secure": false,
        "logLevel": "debug",
        "changeOrigin": false
    }
}

Pero el error persiste y pierdo la mitad del día entre búsquedas, todos dan consejos razonables, todos los pruebo cada vez con menos entusiasmo, y me digo: "esto ya lo viví cientos de veces".

La solución (otra vez)

Lo acabo de resolver, y ahora lo voy a documentar para Juan Pablo del futuro.

Toda solución parece evidente cuando la encuentras. Explorando vi algo que podría ser la falla...

Mi baseURL está apuntando al backend, y tengo un proxy que también lo hace. Uno de los dos está mal, de lo contrario no necesitaría el proxy.

Así que, modificando el baseUrl a http://localhost:4200 —el puerto original de Angular— se solucionó el problema.

Cabe mencionar que también configuré el servicio para soportar CORS:

	// Register CORS middleware to allow Angular frontend requests
	pb.OnBeforeServe().Add(func(e *core.ServeEvent) error {
		app.Log.Info("CORS middleware ---")
		e.Router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
			AllowOrigins:     []string{"http://localhost:4200", "http://localhost:8090"},
			AllowHeaders:     []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
			AllowMethods:     []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete, http.MethodOptions},
			AllowCredentials: true,
			// Ensure OPTIONS requests are handled correctly
			MaxAge: 86400, // Optional: cache preflight results for 24 hours
		}))

		return nil
	})

Esto ya lo había implementado antes eh, no creas que no estaba cuando el navegador se rehusaba a la conexión pero lo pongo ahora por si alguien pensó que me faltaba.

Lección aprendida (otra vez)

La moraleja es simple: cuando uses un proxy para desarrollo, asegúrate de que tu baseUrl apunte al servidor de desarrollo de Angular (localhost:4200), no al backend directamente. El proxy se encargará de redirigir las peticiones al backend apropiadamente.

export const environment = {
  production: false,
  protocol: 'http',
  baseUrl: 'http://localhost:4200/',  // <-- aquí fíjate!
};

Espero que este recordatorio me ahorre tiempo la próxima vez que me encuentre maldiciendo al error de CORS.

jueves, febrero 13, 2025

Continuando la transición: De Flask a GoFiber

Una ventaja de ser expulsado de tu zona de confort —TurboGears, Genshi, SQLAlchemy— es que te obliga a estudiar y analizar bien cómo quieres que funcionen tus futuros desarrollos. ¿Quieres seguir usando Apache-WSGI? ¿Ya te cansaste de estar pendiente de las actualizaciones de los servidores? ¿Lidiar con dependencias descontinuadas?

Flask parecía la opción más obvia para sustituir a TurboGears, así que comenzamos a migrar algunos desarrollos. La curva de aprendizaje no parecía muy pronunciada. También aprovechamos la oportunidad para irnos hacia una arquitectura serverless, hospedando las aplicaciones en contenedores usando Docker. Ya teníamos experiencia con contenedores al desplegar los proyectos anteriores en TurboGears.

Los contenedores para Flask incluían Nginx, WSGI y el código fuente del proyecto. ~370MB parecían razonables y casi mágicos en ese entonces. Una de las muchas ventajas de Python es que puedes modificar el código mientras está corriendo en producción 🤓. Un error ortográfico, un hotfix, etc., se podían corregir en minutos: abrir una sesión de sh o bash dentro del contenedor, instalar vim, modificar el archivo en cuestión y listo. Después, la corrección se aplicaba al código base y el contenedor quedaba limpio en la siguiente actualización.

Durante la cuarentena de 2020, comencé a utilizar Go para desarrollar servicios. La sintaxis es muy simple, aunque a veces repetitiva. También me costó un poco al principio adaptarme a la falta de un mecanismo tradicional de manejo de excepciones. En Python, Java, JavaScript y otros lenguajes puedes envolver un segmento de código dentro de un try-catch. Siguiendo el principio "It is easier to ask for forgiveness than permission", puedes manejar la excepción cuando lo que crees que va a pasar no sucede.

En Go, tienes que pensarlo de una forma diferente.


Manejo de errores: Python vs. Go

Try-Catch en Python

En Python, el manejo de errores es sencillo con try-except:

def dividir(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Error: No se puede dividir entre cero.")
        return None

resultado = dividir(10, 0)  # Esto imprimirá un mensaje de error

Manejo de errores en Go

Go no tiene un try-catch convencional. En su lugar, el manejo de errores se realiza mediante valores de retorno:

package main

import (
    "errors"
    "fmt"
)

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("no se puede dividir entre cero")
    }
    return a / b, nil
}

func main() {
    resultado, err := dividir(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Resultado:", resultado)
    }
}
A medida que avanzábamos en la adopción de Go, nos dimos cuenta de que la simplicidad y el rendimiento que ofrecía valían el esfuerzo de adaptación. Aunque al principio resultaba extraño no contar con un sistema de excepciones tradicional, el manejo explícito de errores nos obligó a escribir código más robusto y predecible. Con el tiempo, terminamos prefiriendo esta aproximación, ya que facilitaba la depuración y nos ayudó a construir servicios más eficientes y escalables. Así, lo que comenzó como una migración obligada, terminó transformándose en una mejora significativa en nuestra forma de desarrollar aplicaciones. 🚀

lunes, febrero 03, 2025

Agregando timestamps a fotos

Usando mogrify, parte de las herramientas de imagemagick, podemos agregar el timestamps a fotos que tiene la fecha de captura en su EXIF.

$ mogrify  -pointsize 28 -gravity SouthEast -annotate +5+5 '%[EXIF:DateTime]'  *.jpg

Mas información de configuración en: http://www.imagemagick.org/Usage/basics/#mogrify

Cuando tu plataforma favorita es descontinuada

Hace varios años, cuando llegó el momento de elegir una plataforma para sustituir al tremendo Java, con sus JSF, Spring y demás, dimos el salto a Python. La razón principal era su rapidez para desarrollar prototipos, lo que permitía que los clientes pudieran visualizar y aprobar sus proyectos con mayor agilidad.

En ese entonces, había dos propuestas destacadas: Django y TurboGears. Django era la opción obvia, pero no me convencía que todo estuviera integrado dentro del mismo ecosistema: el ORM, el renderizado de plantillas y el enrutamiento. TurboGears, por otro lado, ofrecía herramientas de código abierto para cada una de estas tareas.

Para mí, la mejor opción era TurboGears, y durante muchos años nos funcionó de maravilla. Se desarrollaron numerosos proyectos con esta tecnología: era rápido y fácil de adaptar.

Sin embargo, con la llegada de Python 3.0, las actualizaciones comenzaron a retrasarse. Luego vino Python 3.4 y la situación empeoró; con Python 3.8, simplemente dejaron de existir. TurboGears había quedado abandonado.

Aun así, logramos adaptar nuestros desarrollos para funcionar en contenedores con Python 3.8, la última versión en la que TurboGears aún operaba sin demasiados problemas.

Esperamos en vano una actualización, mientras Django seguía evolucionando.

Durante la cuarentena de 2020 aprovechamos para explorar nuevas opciones. Flask parecía una buena alternativa... pero, en medio del enorme esfuerzo de refactorización, apareció una opción aún mejor: GoFiber y GORM.

viernes, abril 29, 2016

Timestamps con SQLAlchemy

Es común tener campos que guarden la fecha de la creación de registro y la fecha de modificación de ése registro.

Mucho tiempo usé la siguiente declaración:

1
2
    ts = Column(u'ts', TIMESTAMP(timezone=False),
                server_onupdate=text('CURRENT_TIMESTAMP'), primary_key=False, nullable=False)

Aprovechando que 'CURRENT_TIMESTAMP' es soportado en MySQL, cada vez que se inserta un registro MySQL pone la fecha del servidor de manera automática.

Creo que ahora hay una mejor manera de manejarlo, siempre y cuando el RDBMS soporte 'on update':


1
2
    created = Column(DateTime, nullable=False, default=func.now())
    updated = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now())

Es mas simple y fácil de leer

domingo, abril 24, 2016

Como quitar atributos extendidos en OSX (com.apple.quarantine)

Recientemente al bajar una nueva plantilla y descomprimirla en un nuevo directorio, aparece que con un '@' a un lado de sus permisos de archivo.
jupabeans at TankerMini in ~/workspace/theme
$ ls -ls

total 0

0 drwxr-xr-x     4 jupabeans  staff   136 Apr 24 22:07 .

0 drwxr-xr-x     7 jupabeans  staff   238 Apr 24 21:56 ..

0 drwxrwxrwx@    6 jupabeans  staff   204 May 28  2015 README

0 drwxrwxrwx@  121 jupabeans  staff  4114 May 28  2015 theme
jupabeans at TankerMini in ~/workspace/theme
$ ls -@ls

total 0

0 drwxr-xr-x     4 jupabeans  staff   136 Apr 24 22:07 .

0 drwxr-xr-x     7 jupabeans  staff   238 Apr 24 21:56 ..

0 drwxrwxrwx@    6 jupabeans  staff   204 May 28  2015 README

     com.apple.quarantine 26
0 drwxrwxrwx@  121 jupabeans  staff  4114 May 28  2015 theme
     com.apple.quarantine 26
Eso indica que el archivo tiene atributos extendidos (extended attributes) en OSX.  En éste caso 'com.apple.quarantine', una bandera que OSX usa desde Leopard (10.5) para marcar archivos descargados de fuentes no confiables.  Cuando ejecutas por primera vez ese archivo, una pantalla de confirmación aparecerá en la pantalla.

Pero ¿qué pasa cuando sólo son archivos de texto? pues nada, pero siempre quedarán marcados y a mi me distraen.  Así que los voy a quitar.

Hay varios métodos para quitarlos, el inteligente sería, buscar los archivos marcados en un directorio y quitarlos uno por uno.  El brutal sería quitarlos con 'recursive' activado.
$ xattr -dr com.apple.quarantine theme
xattr es el comando para manejar los atributos extendidos, -d es la opción para borrarlo -r activa recursividad, luego el nombre del atributo y al final el directorio o archivo.

Hay varias plantillas que tienen los archivos marcados como ejecutables, para quitarlos simplemente:

$ chmod 755 $(find theme -type d)
$ chmod 644 $(find theme -type f)
En ocasiones los espacios en los nombres de archivos pueden confundir los argumentos de chmod. Se puede usar las siguientes alternativas.
$ find theme -type d -exec echo -n '"{}" ' \; | xargs chmod 755
$ find theme -type f -exec echo -n '"{}" ' \; | xargs chmod 644
*actualizado 22 noviembre 2016
Otro método para remover los atributos extendidos
$ xattr -rc .

miércoles, febrero 24, 2016

Mercurial y control de versiones

En cada proyecto de desarrollo debemos contar con una herramienta para el control de versiones. Mercurial es actualmente mi favorito, hace años fue cvs y luego svn.

Haremos un simple ejercicio para mostrar funciones básicas de uso frecuente con ésta herramienta.  No he probado como funcionarían éstos ejemplos en Windows, pero si funcionan en sistemas POSIX.  Unix, Linux y OS X.

martes, febrero 23, 2016

Creando un ambiente de desarrollo en OS X

OS X es una buena plataforma de desarrollo.  Para todos los propósitos es una tiene un API POSIX, que la hace similar a al ambiente de desarrollo que antes usaba en Linux.

Sin embargo, el shell es muy elemental y ya me acostumbré a los beneficios de herramientas que en Linux están activadas por default.  Te muestro algunas de ellas.


lunes, enero 07, 2013

AngularJS, Turbogears 2.2 y RestController

Ahora que AngularJS está en producción, me dispongo a aprovechar esa nueva herramienta, con el respaldo de Turbogears.

En el homepage de AngularJS existe un demo ('Wire up a Backend') que está respaldado por MondoDB de MongoLabs y aunque funciona excelente y la plataforma de MongoDB parece muy sólida, yo quise experimentar con otras herramientas.

miércoles, mayo 18, 2011

Python, Genshi, fechas y UnicodeDecodeError

Hace unas semanas agregué el locale es_MX.UTF-8 para publicar fechas en español. Me llevó poco más de una hora descubrir un error en una aplicación que había funcionado perfectamente ayer. Los errores de python no siempre son los más claros.

La causa, 'UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 2: ordinal not in range(128)', yo creo el error más odiado para los hispano parlantes de python. La función que uso para publicar la fecha de hoy en Genshi es:
...
<link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/tables.css')}" />
</head>
<?python
import locale
locale.setlocale(locale.LC_ALL, 'es_MX.utf8')
import datetime
?>
<body>
 <div id="wrap">
  <h2>
  Cotizaci&oacute;n
   <span>${datetime.datetime.now().strftime("%a %d de %B del %Y").upper()}</span>
  </h2>
  <div id="navcontainer">
...

Nunca se me ocurrió que tenía que preocuparme por días como hoy, Miércoles.
La función queda así:
...
<link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/tables.css')}" />
</head>
<?python
import locale
locale.setlocale(locale.LC_ALL, 'es_MX.utf8')
import datetime
?>
<body>
 <div id="wrap">
  <h2>
  Cotizaci&oacute;n
   <span>${datetime.datetime.now().strftime("%a %d de %B del %Y").decode('utf-8').upper()}</span>
  </h2>
  <div id="navcontainer">
...


Agregar .decode('utf-8') funciona la mayoría de las veces que se presenta el UnicodeDecodeError.

miércoles, mayo 11, 2011

Importando desde CSV con Python

Siendo Excel la herramienta más común en todas las pequeñas y medianas empresas, es casi ley que los proyectos inicien con datos ya capturados en tablas. Estas tablas las exporto y las ordeno de acuerdo a mis necesidades.
Comúnmente las exporto a CSV, delimitado por coma con doble comilla como separador.
Creo mi modelo en forma declarativa.
class CoberturaCame(DeclarativeBase):
    """ Beneficios Adicionales """
    __tablename__ = "came"
    
    #{ Columns
    
    id = Column(Integer, autoincrement=True, primary_key=True)    
    tipo = Column(Integer)
    vendedor = Column(Unicode(32))
    edad_inf = Column(u'edad_inf', Integer, nullable=True)
    edad_sup = Column(u'edad_sup', Integer, nullable=True)
    hombre = Column(Float(precision=2), default=0.0)
    mujer = Column(Float(precision=2), default=0.0)
    
    #}
    
    #{ Helpers

    def from_csv_row(self, row):
        self.tipo = row[1]
        self.vendedor = row[2]
        self.edad_inf = row[3]
        self.edad_sup = row[4]
        self.hombre = row[5]
        self.mujer = row[5]
            
    
    @classmethod
    def by_edad_tipo(cls, edad, tipo):
        """Return the BenAdicionales object whose edad is between``edad``."""
        return DBSession.query(cls).filter(
            and_(cls.edad_inf <= edad, 
                cls.edad_sup >= edad,
                cls.tipo==tipo)).first()
        
    #}

y agrego una simple rutina de importación.

from miproyecto import model
import cvs

csvreader = csv.reader(open('res/janem_cobertura_came.csv'))
# skip first row
csvreader.next()
for row in csvreader:
    if len(row) == 0:
        continue
    d = model.CoberturaCame()
    
    d.from_csv_row(row)
    
    model.DBSession.add(d)

model.DBSession.flush()

La función miembro from_csv_row(row) definida en el modelo, es una ayuda visual y no tiene otro propósito mas que copiar los valores de la fila a un objeto nuevo. En algunos casos pudiera servir para realizar alguna transformación como fechas, minúsculas y mayúsculas o alguna operación aritmética antes de entrar. Aunque yo aconsejo realizar todas esas transformaciones desde la tabla de Excel y exportar el CSV ya listo para importar.

Cabe mencionar que las cantidades numéricas deben de estar sin formato, por que los caractéres '$' o las ',' dentro de las cifras confunden el lector de python.

martes, mayo 03, 2011

Forzando phpmyadmin sobre SSL en Ubuntu Lucid (10.04)

Un día me quedé sin conexión a Internet así que decidí tomar prestada la de algún vecino.  Después de algunas horas de trabajo, me vino un extraño pensamiento ... '¿y si mi vecino captura todos los passwords que pasan por su red como lo hago yo?' He entrado a los phpmyadmins de tres servidores haciendo modificaciones, fácilmente pudo capturar tres credenciales de root.

Resulta que no está bien documentado en la red cómo asegurar phpmyadmin en un Ubuntu Lucid.

Tengo que crear las llaves de seguridad
# apt-get update
# apt-get upgrade
# apt-get install openssl
# mkdir /etc/ssl/localcerts
# openssl req -new -x509 -days 365 -nodes -out /etc/ssl/localcerts/apache.pem -keyout /etc/ssl/localcerts/apache.key
# chmod 600 /etc/ssl/localcerts/apache*

Habilitar el módulo SSL del Apache
# cd /etc/apache2/mods-enabled
# ln -s ../mods-available/ssl.* .

Habilitar el sitio por default SSL de Apache
# cd /etc/apache2/sites-enabled
# ln -s ../sites-available/default-ssl .

Modificar default-ssl
<IfModule mod_ssl.c>
- <VirtualHost _default_:443>
------
  <IfModule mod_ssl.c>
+ <VirtualHost *:443>

Modificar /etc/apache2/ports.conf
<IfModule mod_ssl.c>
    # If you add NameVirtualHost *:443 here, you will also have to change
    # the VirtualHost statement in /etc/apache2/sites-available/default-ssl
    # to <VirtualHost *:443>
    # Server Name Indication for SSL named virtual hosts is currently not
    # supported by MSIE on Windows XP.
+   NameVirtualHost *:443
    Listen 443
</IfModule>

Adaptar /etc/apache2/conf.d/phpmyadmin.conf, aquí yo envuelvo la configuración dentro de un VirtualHost con el nombre de mi servidor, en este ejemplo my.servername.com
+<VirtualHost *:443>
+       ServerName my.servername.com
+       ServerAdmin webmaster@servername.com
+
+ DocumentRoot /var/www
  Alias /phpmyadmin /usr/share/phpmyadmin
al inicio y
+   #   SSL Engine Switch:
+   #   Enable/Disable SSL for this virtual host.
+   SSLEngine on
+
+   #   A self-signed (snakeoil) certificate can be created by installing
+   #   the ssl-cert package. See
+   #   /usr/share/doc/apache2.2-common/README.Debian.gz for more info.
+   #   If both key and certificate are stored in the same file, only the
+   #   SSLCertificateFile directive is needed.
+   SSLCertificateFile    /etc/ssl/localcerts/apache.pem
+   SSLCertificateKeyFile /etc/ssl/localcerts/apache.key
+</VirtualHost>
al final. Reiniciamos el servidor y probamos.

Parece mucho trabajo para algo que debería de ser aún más simple, sin embargo, los beneficios en seguridad valen la pena.

Mas info en: Creating a Self-Signed Certificate, phpmyadmin Documentation, Configure Apache to support multiple SSL sites on a single IP address

Actualizado 20 enero 2013 me faltó incluir una forma para obligar a phpmyadmin editando config.inc.php

// poner esto hasta abajo
$cfg['ForceSSL'] = true;

miércoles, abril 20, 2011

Subversion. Creando un patch y aplicándolo en otro branch

Cuando manejo dos branches o ramas de un mismo proyecto, me encuentro con bugfixes que deben de quedar en los dos, para ello creo un patch en la versión que arreglé para aplicarlo en la otra versión.
$ svn diff -c 372 > bugfix_patch
372 es la revisión que contiene el bugfix, el archivo creado contiene la información del patch

Index: update_script.py
===================================================================
--- update_script.py (revision 371)
+++ update_script.py (revision 372)
@@ -20,7 +20,7 @@
     ids = model.Product.getAll_filtered()
     
     for i in ids:
-        l.append(i.model)
+        l.append(i.model.strip() if i.model != None else '')
     
     rpc.updateList(l)
 

luego copio el archivo bugfix_patch al working directory de mi otro branch y aplico el patch.
$ patch -p0 < patch2
-p0 hace que los nombres de los archivos contenidos en el patch mantengan completo su path

Mas info en patch manpage

viernes, abril 15, 2011

Subversion. Moviendo un repositorio svn

Necesito mover un repositorio de un servidor a otro. En este caso es un viejo proyecto que no quiero perder que está alojado en un viejo servidor. Parece que en svn la mejor opción es hacer un dump, y luego un load.

$ svnadmin dump /home/svn/crmdev > crmdev.dump
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
* Dumped revision 3.
* Dumped revision 4.
...

* Notar que svnadmin dump sólo funciona para paths y no urls.
Se crea un archivo de 184M
...
184M -rw-r--r-- 1 lukesw lukesw 184M 2011-04-15 11:04 crmdev.dump
...
Lo comprimo...
$ bzip2 crmdev.dump
... y queda en unos 141M
141M -rw-r--r-- 1 pablito pablito 141M 2011-04-15 11:04 crmdev.dump.bz2
Me ahorré 40M de transferencia entre servers, que con la tasa de transferencia de Infinitum son bastante considerables.
$ scp crmdev.dump.bz2 svn@super.newserver.com:/home/svn
En el nuevo servidor creo el repositorio y lo cargo con el dump
$ svnadmin create crmdev
$ svnadmin load crmdev < crmdev.dump
...
<<< Started new transaction, based on original revision 1
     * adding path : trunk/umlconcepts ... done.
     * adding path : trunk/umlconcepts/.classpath ... done.
     * adding path : trunk/umlconcepts/.myumldata ... done.
     * adding path : trunk/umlconcepts/.project ... done.
     * adding path : trunk/umlconcepts/.settings ... done.
     * adding path : trunk/umlconcepts/.settings/com.genuitec.eclipse.core.prefs ... done.
     * adding path : trunk/umlconcepts/uml.umr ... done.
...
<<< Started new transaction, based on original revision 63
     * editing path : trunk/productsweb/WebRoot/WEB-INF/jsp/footer.jsp ... done.

------- Committed revision 63 >>>
Listo, el repositorio ha sido copiado/movido

martes, abril 12, 2011

.hgignore para proyectos Turbogears 2+

Subversion es mi VCS favorito, sin embargo uso mucho Mercurial para cambios pequeños, pruebas de código y parches. Tengo repositorios híbridos (SVN/HG) que me funcionan muy bien. Copio mi archivo .hgignore para futura referencia.
# use glob syntax.
syntax: glob
*.svn   # repositorio híbrido
*.pyc   # compilados
*~      # temporales

syntax: regexp
^lib$   # librería específica del proyecto
* Tengo que hacer una referencia rápida para Mercurial

lunes, abril 04, 2011

Multiple relación a la misma tabla con Declarative SQLAlchemy 0.6

En el manual de SQLAlchemy la solución no es muy clara para la forma declarative, me arrojaba el siguiente error
sqlalchemy.exc.ArgumentError: Column-based expression object expected for argument 'primaryjoin'; got: 'False'
Al ver Column-based expression supuse que era el mismo tipo que en los filtros y así fué. La solución es simple como siempre, la pongo aquí para referencia.
class Registro(DeclarativeBase):
    __tablename__ = 'registro'
    
    #{ Columns    
    id = Column(Integer, primary_key=True)
    # folio único
    folio = Column(Integer, nullable=True)
    medic_id = Column(u'medic_id', Integer, ForeignKey('medic.id'))
    nursea_id = Column(u'nursea_id', Integer, ForeignKey('nurse.id'), nullable=True)
    nurseb_id = Column(u'nurseb_id', Integer, ForeignKey('nurse.id'), nullable=True)
    room_id = Column(u'room_id', Integer, ForeignKey('room.id'))
    status_id = Column(u'status_id', Integer, ForeignKey('status.id'))
    fecha = Column(u'fecha', Date(timezone=False), primary_key=False, nullable=True)
    patient = Column(u'patient', Unicode(255), nullable=False)
    patient_age = Column(u'patient_age', Integer, nullable=True)
    mat = Column(u'mat', Unicode(255), nullable=False)
    kit_id = Column(u'kit_id', Integer, ForeignKey('kit.id'))
    hour_in = Column(u'hour_in', Unicode(16), nullable=False)
    hour_out = Column(u'hour_out', Unicode(16), nullable=False)
    diagnosis = Column(u'diagnosis', Text(length=None, convert_unicode=True, \
        assert_unicode=None), primary_key=False)
    procedure = Column(u'procedure', Unicode(255), nullable=False)
    notes = Column(u'notes', Text(length=None, convert_unicode=True, \
        assert_unicode=None), primary_key=False)
la relación dentro la misma clase queraría así
    #{ Relations
    
    medic = relation(Medic)
    kit = relation(Kit)
    status = relation(Status)
    room = relation(Room)
    nursea = relation(Nurse, primaryjoin=nursea_id == Nurse.id)
    nurseb = relation(Nurse, primaryjoin=nurseb_id == Nurse.id)
    
    #}

viernes, abril 01, 2011

Python y locale.setlocale

Quiero desplegar las fechas de mis páginas hechas con Turbogears2.0 en español.  Ahora tengo esta salida.

In [5]: datetime.datetime.now().strftime("%a %d de %B del %Y")
Out[5]: 'Fri 01 de April del 2011'

Reviso los locale que tengo instalados en mi servidor, en este caso un Ubuntu Lucid (10.04)

root@kirk:~# locale -a
C
POSIX
en_AG
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_NG
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZW.utf8
root@kirk:~#

Si no está el locale que requiero, lo genero, en este caso es: es_MX.UTF-8

root@kirk:~# /usr/sbin/locale-gen es_MX.UTF-8
Generating locales...
es_MX.UTF-8... done
Generation complete.
root@kirk:~#
La generación de locales en Lucid difiere un poco de como lo hacía en Intrepid
Confirmo la generación del locale

root@kirk:~# locale -a
C
POSIX
en_AG
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_NG
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZW.utf8
es_MX.utf8
root@kirk:~#

Bien, ya tengo un locale en español que puedo utilizar. Lo activo dentro de mi ambiente python y compruebo.

In [6]: import locale

In [7]: locale.setlocale(locale.LC_ALL, 'es_MX.utf8')
Out[7]: 'es_MX.utf8'

In [8]: datetime.datetime.now().strftime("%a %d de %B del %Y")
Out[8]: 'vie 01 de abril del 2011'


Algo realmente simple me entretuvo mucho tiempo, ya que obtenía errores como:

In [9]: locale.setlocale(locale.LC_ALL, 'es_MX')
---------------------------------------------------------------------------
Error Traceback (most recent call last)

/usr/lib/python2.6/locale.pyc in setlocale(category, locale)
511 # convert to string

512 locale = normalize(_build_localename(locale))
--> 513 return _setlocale(category, locale)
514
515 def resetlocale(category=LC_ALL):

Error: unsupported locale setting

... y encontré poca documentación al respecto, lo pongo aquí para futuras referencias.

sábado, julio 03, 2010

rsync - Guía Rápida

Linux tiene muchas herramientas para copiar y respaldar información. rsync es la favorita para muchos administradores.

rsync utiliza un algoritmo que permite transmitir eficientemente una estructura (como un archivo) a través de un canal de comunicación cuando el receptor ya tiene una versión diferente de la misma estructura. (wikipedia)

Copiar localmente

En su forma más simple lo podemos utilizar para copiar directorios enteros de forma local.
$ rsync -av /home/user/Docs /media/usb/backup/
-av lo ejecuta en modo verbose y archivo, práctica común para copia recursiva, conservar permisos y symlinks.

Copiar remotamente

Tanto el origen como el destino pueden ser otra computadora en la red.
$ rsync -avz /home/user/Docs user@online.backups.com:/backups/
agregamos -z para comprimir los datos sobre la red.

rsync puede tambier borrar archivos que ya no se encuentran en el origen pero si en el destino causa de alguna copia anterior. Es decir, que pasa si ya borramos algunos archivos de nuestro directorio de trabajo y queremos que también sean borrados de nuestro respaldo.
$ rsync -avz --del /home/user/Docs user@online.backups.com:/backups/
--del indica a rsync que borre los archivos que ya no están en el origen.

Directorio o archivos

Importante el último slash '/'. en:
$ rsync -avz --del /home/user/Docs user@online.backups.com:/backups/
estoy copiando el directorio 'Docs' dentro del directorio 'backups' pero si
$ rsync -avz --del /home/user/Docs/ user@online.backups.com:/backups/
estaré copiando los archivos dentro del directorio 'Docs' al directorio 'backups'

Copia incremental

$ rsync --backup --backup-dir=`date +%Y.%m.%d` -av /home/user/Docs/ /media/usb/backup/Docs/
--backup crea un respaldo --backup-dir crea el respaldo dentro de la jerarquía del directorio de destino. También se puede usar --suffix=SUFFIX para agregar un sufijo.

Apenas rascando la superficie de esta poderosa utilería. Para más información manpage.

lunes, marzo 22, 2010

MVC en Python

Últimamente he utilizado herramientas como SQLAlchemy y Turbogears en Python. La verdad me ha gustado mucho la flexibilidad y facilidad con la que puedo escribir scripts para procesar información con SQLAlchemy y me ha gustado mucho el modelo MVC de Turbogears 2, parecido al de Spring en Java, por lo que voy a comenzar a implementar más cosas en python.
Comenzaré con un blog/cms en Turbogears, yo sé que existen muchísimas opciones ya desarrolladas que seguramente son mejores a cualquiera que yo pueda hacer, sin embargo, me sirve para familiarizarme en ambiente de trabajo con TG2.

Instalando Turbogears.
La mejor opción es con Virtualenv, que crea un "ambiente virtual" de python para cargar dependencias, librerías y versiones, etc. Sin embargo en producción a mi me ha funcionado instalarlo junto con el resto de python del servidor. Las instrucciones para instalarlo son sencillas y están aquí.
Creando un proyecto
Es muy simple iniciar un proyecto en Turbogears.

$ paster quickstart
Enter project name: Altadefinicion
Enter package name [altadefinicion]:
Do you need authentication and authorization in this project? [yes]
Selected and implied templates:
tg.devtools#turbogears2 TurboGears 2.0 Standard Quickstart Template

Variables:
auth: sqlalchemy
egg: Altadefinicion
geo: None
package: altadefinicion
project: Altadefinicion
sqlalchemy: True
sqlobject: False
tgversion: 2.0.3
Creating template turbogears2
Creating directory ./Altadefinicion
...
...
...
reading manifest template 'MANIFEST.in'
writing manifest file 'Altadefinicion.egg-info/SOURCES.txt'

Si no recibimos ningún mensaje de error, podemos iniciar la aplicación con:

$ paster serve --reload development.ini

Y apuntando nuestro explorador a http://localhost:8080 podremos ver una página de bienvenida.



Entrando al directorio del proyecto podemos observar una simple estructura:

$ tree -d
.
|-- Altadefinicion.egg-info
|-- altadefinicion
| |-- config
| |-- controllers
| |-- i18n
| | `-- ru
| | `-- LC_MESSAGES
| |-- lib
| |-- model
| |-- public
| | |-- css
| | `-- images
| |-- templates
| `-- tests
| |-- functional
| `-- models
|-- data
| `-- sessions
`-- ez_setup

25 directories

Por default TG2 usa SQLite como motor de base de datos y almacena todo en un archivo llamado devdata.db para crear la base de datos por default con datos de prueba hay que correr otro comando de paster:

$ paster setup-app development.ini

Con esto ya tenemos lista la cimentación para construir una aplicación con python, usando Turbogears 2 y SQLalchemy