Qué vas a conseguir
Un script de Python que lee la lista de equipos de un CSV, pregunta a una API REST el estado de cada uno, escribe un informe en CSV y en HTML, deja un log de lo que ha hecho y se ejecuta solo cada mañana. Es el patrón de casi cualquier automatización de operaciones: entrada, consulta, informe, programación. Y lo que hacer cuando funciona en tu equipo y falla en el servidor.
Antes de empezar
- Python 3 de python.org (en Windows, con el lanzador
py) o el del sistema en Linux. - Una carpeta para el script, por ejemplo
C:\scripts\informe-equiposo/opt/informe-equipos. - La URL de la API y un token con permisos de solo lectura, pedido a quien la administra.
- Si la empresa usa proxy con inspección TLS, el certificado de la CA corporativa en formato PEM (te lo da el equipo de red o seguridad).
1. Un entorno virtual por script
Cada script con sus propias librerías, sin tocar el Python del sistema:
cd C:\scripts\informe-equipospy -m venv .venv.\.venv\Scripts\Activate.ps1pip install requestspip freeze > requirements.txtEn Linux, python3 -m venv .venv y source .venv/bin/activate. Con requirements.txt el entorno se rehace en otro servidor (pip install -r requirements.txt); .venv no se copia ni va a Git.
2. El script
import csvimport htmlimport loggingimport osfrom datetime import date import requests API = "https://api.contoso.local/v1/devices"TOKEN = os.environ["INVENTARIO_TOKEN"] # nunca escrito en el códigologging.basicConfig(filename="informe.log", level=logging.INFO, encoding="utf-8", format="%(asctime)s %(levelname)s %(message)s") def estado(equipo): r = requests.get(f"{API}/{equipo}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=15) r.raise_for_status() return r.json() filas = []with open("equipos.csv", newline="", encoding="utf-8-sig") as f: for linea in csv.DictReader(f): nombre = linea["Equipo"] try: d = estado(nombre) filas.append({"Equipo": nombre, "SO": d.get("os"), "Ultimo contacto": d.get("lastSeen")}) except requests.RequestException as e: logging.warning("%s: %s", nombre, e) filas.append({"Equipo": nombre, "SO": "", "Ultimo contacto": "ERROR"})logging.info("%d equipos procesados", len(filas)) salida = f"informe-{date.today()}.csv"with open(salida, "w", newline="", encoding="utf-8-sig") as f: w = csv.DictWriter(f, fieldnames=["Equipo", "SO", "Ultimo contacto"]) w.writeheader() w.writerows(filas) celdas = "".join(f"<tr><td>{html.escape(str(x['Equipo']))}</td><td>{html.escape(str(x['SO']))}</td>" f"<td>{html.escape(str(x['Ultimo contacto']))}</td></tr>" for x in filas)with open(salida.replace(".csv", ".html"), "w", encoding="utf-8") as f: f.write(f"<meta charset='utf-8'><table><tr><th>Equipo</th><th>SO</th><th>Último contacto</th></tr>{celdas}</table>")Lo que importa de cada parte:
encoding="utf-8-sig"al leer: Excel guarda los CSV en UTF-8 con BOM, y sin-sigla primera columna se llama\ufeffEquipoy elKeyErrordesconcierta. Al escribir, el BOM hace que Excel abra bien las tildes.timeout=15: sin él, una API que no responde deja el script colgado para siempre, y la tarea programada nunca termina.raise_for_status()convierte un 401 o un 500 en una excepción que eltryrecoge: un equipo que falla no tumba el informe entero, y queda en el log.
3. El token, en una variable de entorno
El token es una contraseña. No va en el script, ni en el CSV, ni en Git. Se pone en una variable de entorno del usuario que ejecuta el script:
[Environment]::SetEnvironmentVariable("INVENTARIO_TOKEN", "pega-aqui-el-token", "User")# abre una consola nueva para que la veapy -c "import os; print('INVENTARIO_TOKEN' in os.environ)"TrueEn Linux, en un fichero chmod 600 de la cuenta de servicio. Con un almacén de secretos (Key Vault, CyberArk), el token se lee de ahí.
4. Programarlo
En Windows, con el Programador de tareas, usando el Python del entorno virtual y la carpeta del script como directorio de inicio:
$a = New-ScheduledTaskAction -Execute "C:\scripts\informe-equipos\.venv\Scripts\python.exe" -Argument "informe.py" -WorkingDirectory "C:\scripts\informe-equipos"$t = New-ScheduledTaskTrigger -Daily -At 07:00Register-ScheduledTask -TaskName "IT\Informe equipos" -Action $a -Trigger $t -User "CONTOSO\svc-informes" -Password (Read-Host "Contraseña")En Linux, con crontab -e del usuario de servicio:
0 7 * * 1-5 cd /opt/informe-equipos && . ./token.env && .venv/bin/python informe.pySin -WorkingDirectory (o el cd en cron), el script busca equipos.csv en C:\Windows\System32 o en el home, y falla con FileNotFoundError aunque a mano funcionara.
5. El ticket: "a mano funciona, programado falla"
El log de la tarea dice una de estas tres cosas:
KeyError: 'INVENTARIO_TOKEN': la tarea corre con otra cuenta (svc-informes) y la variable se definió para tu usuario. Se define para la cuenta de servicio, o a nivel de máquina si la política lo permite.UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1: el CSV lo ha guardado alguien como CSV (delimitado por comas) en lugar de CSV UTF-8, y viene en cp1252. La solución buena es pedir el fichero en UTF-8; mientras tanto,encoding="cp1252"para ese origen concreto. Si el error esUnicodeEncodeErroral imprimir, es la consola de Windows: la variablePYTHONUTF8=1en la tarea lo evita.ProxyErroroSSLError: CERTIFICATE_VERIFY_FAILED: en tu sesión el proxy se toma de la configuración del usuario y en la tarea no.requestsusa las variablesHTTPS_PROXYyNO_PROXY; y si el proxy inspecciona TLS, necesita la CA corporativa enREQUESTS_CA_BUNDLE.
[Environment]::SetEnvironmentVariable("HTTPS_PROXY", "http://proxy.contoso.local:8080", "Machine")[Environment]::SetEnvironmentVariable("NO_PROXY", "localhost,.contoso.local", "Machine")[Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", "C:\scripts\ca\contoso-root.pem", "Machine")verify=False hace que el error desaparezca y deja la conexión abierta a cualquiera que se ponga en medio, con el token viajando dentro. No se usa, tampoco "para probar".
Siguientes pasos
- La guía de PowerShell cubre lo mismo en el lenguaje nativo de Windows; muchas tareas de IT se hacen mejor en uno u otro, y conviene conocer los dos.
- Microsoft Graph: la misma estructura de script sirve para leer equipos de Intune o usuarios de Entra ID, con un token de aplicación en lugar de uno fijo.
- Cuando tienes varios de estos scripts, el siguiente paso es una consola que los reúna. Es la idea del repositorio 05-it-ops-console de mi GitHub.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
What you'll get
A Python script that reads a list of devices from a CSV, asks a REST API for each one's status, writes a report as CSV and HTML, leaves a log of what it did and runs by itself every morning. It is the pattern of almost any operations automation: input, query, report, schedule. And what to do when it works on your machine and fails on the server.
Before you start
- Python 3 from python.org (on Windows, with the
pylauncher) or the system one on Linux. - A folder for the script, for example
C:\scripts\device-reportor/opt/device-report. - The API URL and a read-only token, requested from whoever administers it.
- If the company uses a proxy with TLS inspection, the corporate CA certificate in PEM format (the network or security team will give it to you).
1. One virtual environment per script
Each script with its own libraries, without touching the system Python:
cd C:\scripts\device-reportpy -m venv .venv.\.venv\Scripts\Activate.ps1pip install requestspip freeze > requirements.txtOn Linux, python3 -m venv .venv and source .venv/bin/activate. With requirements.txt the environment is rebuilt on another server (pip install -r requirements.txt); .venv is not copied or put in Git.
2. The script
import csvimport htmlimport loggingimport osfrom datetime import date import requests API = "https://api.contoso.local/v1/devices"TOKEN = os.environ["INVENTORY_TOKEN"] # never written in the codelogging.basicConfig(filename="report.log", level=logging.INFO, encoding="utf-8", format="%(asctime)s %(levelname)s %(message)s") def status(device): r = requests.get(f"{API}/{device}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=15) r.raise_for_status() return r.json() rows = []with open("devices.csv", newline="", encoding="utf-8-sig") as f: for line in csv.DictReader(f): name = line["Device"] try: d = status(name) rows.append({"Device": name, "OS": d.get("os"), "Last seen": d.get("lastSeen")}) except requests.RequestException as e: logging.warning("%s: %s", name, e) rows.append({"Device": name, "OS": "", "Last seen": "ERROR"})logging.info("%d devices processed", len(rows)) out = f"report-{date.today()}.csv"with open(out, "w", newline="", encoding="utf-8-sig") as f: w = csv.DictWriter(f, fieldnames=["Device", "OS", "Last seen"]) w.writeheader() w.writerows(rows) cells = "".join(f"<tr><td>{html.escape(str(x['Device']))}</td><td>{html.escape(str(x['OS']))}</td>" f"<td>{html.escape(str(x['Last seen']))}</td></tr>" for x in rows)with open(out.replace(".csv", ".html"), "w", encoding="utf-8") as f: f.write(f"<meta charset='utf-8'><table><tr><th>Device</th><th>OS</th><th>Last seen</th></tr>{cells}</table>")What matters in each part:
encoding="utf-8-sig"when reading: Excel saves CSVs as UTF-8 with a BOM, and without-sigthe first column is called\ufeffDeviceand theKeyErroris baffling. When writing, the BOM makes Excel open accented characters correctly.timeout=15: without it, an API that doesn't respond leaves the script hanging forever, and the scheduled task never finishes.raise_for_status()turns a 401 or a 500 into an exception thetrycatches: one failing device doesn't bring down the whole report, and it ends up in the log.
3. The token, in an environment variable
The token is a password. It doesn't go in the script, the CSV or Git. It goes in an environment variable of the user who runs the script:
[Environment]::SetEnvironmentVariable("INVENTORY_TOKEN", "paste-the-token-here", "User")# open a new console so it picks it uppy -c "import os; print('INVENTORY_TOKEN' in os.environ)"TrueOn Linux, in a chmod 600 file owned by the service account. With a secrets store (Key Vault, CyberArk), the token is read from there.
4. Scheduling it
On Windows, with Task Scheduler, using the virtual environment's Python and the script folder as the start directory:
$a = New-ScheduledTaskAction -Execute "C:\scripts\device-report\.venv\Scripts\python.exe" -Argument "report.py" -WorkingDirectory "C:\scripts\device-report"$t = New-ScheduledTaskTrigger -Daily -At 07:00Register-ScheduledTask -TaskName "IT\Device report" -Action $a -Trigger $t -User "CONTOSO\svc-reports" -Password (Read-Host "Password")On Linux, with the service user's crontab -e:
0 7 * * 1-5 cd /opt/device-report && . ./token.env && .venv/bin/python report.pyWithout -WorkingDirectory (or the cd in cron), the script looks for devices.csv in C:\Windows\System32 or the home folder, and fails with FileNotFoundError even though it worked by hand.
5. The ticket: "it works by hand, it fails when scheduled"
The task's log says one of these three things:
KeyError: 'INVENTORY_TOKEN': the task runs under another account (svc-reports) and the variable was defined for your user. Define it for the service account, or at machine level if policy allows.UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1: someone saved the CSV as CSV (Comma delimited) instead of CSV UTF-8, and it comes in cp1252. The proper fix is to ask for the file in UTF-8; in the meantime,encoding="cp1252"for that specific source. If the error isUnicodeEncodeErrorwhen printing, it is the Windows console: thePYTHONUTF8=1variable on the task avoids it.ProxyErrororSSLError: CERTIFICATE_VERIFY_FAILED: in your session the proxy comes from the user's settings and in the task it doesn't.requestsuses theHTTPS_PROXYandNO_PROXYvariables; and if the proxy inspects TLS, it needs the corporate CA inREQUESTS_CA_BUNDLE.
[Environment]::SetEnvironmentVariable("HTTPS_PROXY", "http://proxy.contoso.local:8080", "Machine")[Environment]::SetEnvironmentVariable("NO_PROXY", "localhost,.contoso.local", "Machine")[Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", "C:\scripts\ca\contoso-root.pem", "Machine")verify=False makes the error go away and leaves the connection open to anyone who sits in the middle, with the token travelling inside it. It is not used, not even "to test".
Next steps
- The PowerShell guide covers the same in Windows' native language; many IT tasks are better done in one or the other, and it pays to know both.
- Microsoft Graph: the same script structure works for reading Intune devices or Entra ID users, with an application token instead of a fixed one.
- Once you have several of these scripts, the next step is a console that brings them together. That is the idea behind the 05-it-ops-console repository on my GitHub.
Is something missing, or different in your environment? Get in touch and I'll fix it.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.
Apúntate y llévate las 33 ideasSign up and get the 33 ideas →