observability.svc.plus/bin/inventory_load
2026-02-01 20:53:55 +08:00

256 lines
9.3 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*- #
#==============================================================#
# File : inventory_load
# Desc : load pigsty config into cmdb structure
# Ctime : 2022-05-05
# Mtime : 2026-01-11
# Path : bin/inventory_cmdb
# Deps : argparse, os, json, yaml, platform
# License : Apache-2.0 @ https://pigsty.io/docs/about/license/
# Copyright : 2018-2026 Ruohang Feng / Vonng (rh@vonng.com)
#==============================================================#
__author__ = 'Vonng (rh@vonng.com)'
from argparse import ArgumentParser
import os, json, yaml, platform
def usage():
print("""
bin/load_conf [ -p | --path = ${PIGSTY_HOME}/pigsty.yml ]
[ -d | --data = ${METADB_URL} ]
Load config into cmdb pigsty schema
""")
SQL_CLEAN_UP = '''TRUNCATE pigsty.group, pigsty.host, pigsty.group_var, pigsty.host_var, pigsty.global_var;'''
SQL_INSERT_GLOBAL_VARS = '''INSERT INTO pigsty.global_var (key, value) VALUES (%s, %s) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, mtime = now();'''
SQL_INSERT_GROUP_NAMES = '''INSERT INTO pigsty.group (cls) VALUES (%s) ON CONFLICT (cls) DO UPDATE SET mtime = now();'''
SQL_INSERT_GROUP_VARS = '''INSERT INTO pigsty.group_var (cls, key, value) VALUES (%s, %s, %s) ON CONFLICT (cls, key) DO UPDATE SET value = EXCLUDED.value, mtime = now();'''
SQL_INSERT_HOST_NAMES = '''INSERT INTO pigsty.host (cls, ip) VALUES (%s ,%s) ON CONFLICT (cls, ip) DO UPDATE SET mtime = now();'''
SQL_INSERT_HOST_VARS = '''INSERT INTO pigsty.host_var (cls, ip, key, value) VALUES (%s, %s, %s, %s) ON CONFLICT (cls, ip, key) DO UPDATE SET value = EXCLUDED.value, mtime = now();'''
###########################
# parse arguments
###########################
DEFAULT_PGURL = ''
DEFAULT_CONFIG_PATH = ''
PIGSTY_HOME = ''
if 'METADB_URL' in os.environ:
DEFAULT_PGURL = os.environ['METADB_URL']
if 'PIGSTY_HOME' in os.environ:
PIGSTY_HOME = os.environ['PIGSTY_HOME']
elif 'HOME' in os.environ:
PIGSTY_HOME = os.path.join(os.environ['HOME'], 'pigsty')
if PIGSTY_HOME != '':
DEFAULT_CONFIG_PATH = os.path.join(PIGSTY_HOME, 'pigsty.yml')
parser = ArgumentParser(description="load config arguments")
parser.add_argument('-n', "--name", default='pgsql', help="config profile name, pgsql by default")
parser.add_argument('-p', "--path", default=DEFAULT_CONFIG_PATH,
help="config path, ${PIGSTY_HOME}/pigsty.yml by default")
parser.add_argument('-d', "--data", default=DEFAULT_PGURL, help="postgres cmdb pgurl, ${METADB_URL} by default")
###########################
# parse config
###########################
def parse_conf(path):
return json.loads(json.dumps(yaml.safe_load(open(path, 'r'))))
def use_dynamic_inventory(ansible_cfg):
if not os.path.exists(ansible_cfg):
raise "%s not exists" % ansible_cfg
cmd = """sed -ie 's/inventory.*/inventory = inventory.sh/g' %s""" % ansible_cfg
if os.system(cmd) != 0:
raise "fail to edit %s" % ansible_cfg
os.remove(ansible_cfg + 'e') # remove sed trash
def use_static_inventory(ansible_cfg):
if not os.path.exists(ansible_cfg):
raise "%s not exists" % ansible_cfg
cmd = """sed -ie 's/inventory.*/inventory = pigsty.yml/g' %s""" % ansible_cfg
if os.system(cmd) != 0:
raise "fail to edit %s" % ansible_cfg
os.remove(ansible_cfg + 'e') # remove sed trash
def write_inventory_sh(path):
with open(path) as dst:
dst.write("#!/bin/bash\npsql service=meta -AXtwc 'SELECT text FROM pigsty.inventory;'")
os.chmod(path, 0o755)
############################
# upsert config and activate
############################
def load_conf(conf, pgurl):
if 'all' not in conf: return
import psycopg2
from psycopg2.extras import Json
groups, global_vars = {}, []
if 'children' in conf['all']: groups = conf['all']['children']
if 'vars' in conf['all']: global_vars = [(k, Json(v)) for k, v in conf['all']['vars'].items()]
group_names = [(i,) for i in groups.keys()]
conn = psycopg2.connect(pgurl)
try:
with conn.cursor() as cur:
cur.execute(SQL_CLEAN_UP)
print("[INFO] truncate pigsty schema")
cur.executemany(SQL_INSERT_GLOBAL_VARS, global_vars)
print("[INFO] load all.vars")
cur.executemany(SQL_INSERT_GROUP_NAMES, group_names)
for cls, group in groups.items():
print("[INFO] load group %s" % cls)
group_vars = []
if 'vars' in group: group_vars = [(cls, k, Json(v)) for k, v in group['vars'].items()]
cur.executemany(SQL_INSERT_GROUP_VARS, group_vars) # insert group var
cur.executemany(SQL_INSERT_HOST_NAMES, [(cls, ip) for ip in group['hosts'].keys()]) # insert host names
for ip, vars in group['hosts'].items():
host_vars = [(cls, ip, k, Json(v)) for k, v in vars.items()]
cur.executemany(SQL_INSERT_HOST_VARS, host_vars) # insert group var
conn.commit()
except:
print("[ERRO] fail to load inventory")
conn.rollback()
raise
############################
# OS-specific default vars
############################
# mapping: xxx_default -> xxx in pigsty.default_var
OS_DEFAULT_PARAMS = {
'repo_packages_default': 'repo_packages',
'repo_extra_packages_default': 'repo_extra_packages',
'node_packages_default': 'node_packages',
'infra_packages_default': 'infra_packages',
'repo_upstream_default': 'repo_upstream',
}
SQL_UPDATE_DEFAULT_VAR = '''UPDATE pigsty.default_var SET value = %s WHERE key = %s;'''
SQL_TRUNCATE_PACKAGE_MAP = '''TRUNCATE pigsty.package_map;'''
SQL_INSERT_PACKAGE_MAP = '''INSERT INTO pigsty.package_map (key, value) VALUES (%s, %s);'''
def detect_os():
"""Detect OS vendor, version and architecture, return (prefix, fallback, arch) or (None, None, None)"""
arch = platform.machine()
if arch == 'arm64':
arch = 'aarch64' # macOS uses arm64
os_release = '/etc/os-release'
if not os.path.exists(os_release):
return (None, None, arch)
os_info = {}
try:
with open(os_release, 'r') as f:
for line in f:
line = line.strip()
if '=' in line:
parts = line.split('=', 1)
if len(parts) == 2:
k, v = parts
os_info[k] = v.strip('"').strip("'")
except Exception:
return (None, None, arch)
vendor = os_info.get('ID', '').lower()
version = os_info.get('VERSION_ID', '')
if '.' in version:
version = version.split('.')[0]
# determine OS prefix and fallback
if vendor in ('rhel', 'centos', 'rocky', 'alma', 'almalinux', 'fedora', 'ol', 'anolis', 'openeuler'):
prefix = 'el' + version
fallback = 'el9'
elif vendor == 'debian':
prefix = 'd' + version
fallback = 'd12'
elif vendor == 'ubuntu':
prefix = 'u' + version
fallback = 'u22'
else:
return (None, None, arch)
return (prefix, fallback, arch)
def load_os_defaults(pgurl):
"""Load OS-specific default parameters into pigsty.default_var"""
if not pgurl or not PIGSTY_HOME:
return
prefix, fallback, arch = detect_os()
if prefix is None:
return # silently skip on non-Linux or unknown OS
vars_dir = os.path.join(PIGSTY_HOME, 'roles', 'node_id', 'vars')
vars_file = os.path.join(vars_dir, '%s.%s.yml' % (prefix, arch))
# fallback if exact match not found
if not os.path.exists(vars_file):
vars_file = os.path.join(vars_dir, '%s.%s.yml' % (fallback, arch))
if not os.path.exists(vars_file):
print("[INFO] skip os-specific defaults: %s.%s.yml not found" % (prefix, arch))
return
# parse vars file
os_vars = None
try:
with open(vars_file, 'r') as f:
os_vars = yaml.safe_load(f)
except Exception as e:
print("[WARN] fail to parse %s: %s" % (vars_file, e))
return
if not os_vars:
return
# collect default_var updates
updates = []
for src_key, dst_key in OS_DEFAULT_PARAMS.items():
if src_key in os_vars:
updates.append((json.dumps(os_vars[src_key]), dst_key))
# collect package_map entries
pkg_map = []
if 'package_map' in os_vars and isinstance(os_vars['package_map'], dict):
for k, v in os_vars['package_map'].items():
pkg_map.append((k, str(v)))
if not updates and not pkg_map:
return
# apply updates to database
try:
import psycopg2
conn = psycopg2.connect(pgurl)
cur = conn.cursor()
try:
if updates:
cur.executemany(SQL_UPDATE_DEFAULT_VAR, updates)
if pkg_map:
cur.execute(SQL_TRUNCATE_PACKAGE_MAP)
cur.executemany(SQL_INSERT_PACKAGE_MAP, pkg_map)
conn.commit()
print("[INFO] load os-specific defaults from %s" % os.path.basename(vars_file))
finally:
cur.close()
conn.close()
except Exception as e:
print("[WARN] fail to update os defaults: %s" % e)
if __name__ == '__main__':
args = parser.parse_args()
print("[INFO] load pigsty config from %s into %s" % (args.path, args.data))
conf = parse_conf(args.path)
load_conf(conf, args.data)
load_os_defaults(args.data)