Автоматизация задач инженера/администратора при помощи Python


В предыдущей статье из цикла по Brocade я обещал рассказать об автоматизации рутинных задач инженера/администратора. Самое главное — я не буду учить вас программировать, потому что я сам делаю это плохо. Если более-менее опытный разработчик на Python посмотрит на мой код, наверное ещё пару ночей ему будут сниться кошмары. Всё дело в том, что я никогда не занимался программированием более чем хобби и у меня не было возможности с кем-то общаться по поводу качества и красоты моего кода, мне не у кого было спросить совета (а на форумах разработчиков не очень любят учить новичков, обычно их отправляют читать документацию). Но этот код работает и он выполняет возлагаемые на него задачи и это является для меня главным.

На самом деле автоматизация любых задач стала меня интересовать довольно давно. А моему первому в этом плане проекту — TorrentMonitor на сегодняшний день уже 6,5 лет.
И так, сегодня на конкретном примере я постараюсь рассказать о том, как можно работать с различным оборудованием при помощи Python, как этим облегчить свою работу и жизнь. Но Python не панацея. Всё зависит от ваших текущих знаний или какие знания и опыт вы хотели бы получить. Есть приверженцы Bash/Powershell. Кто-то говорит, что это хороший вариант. На мой взгляд — проще учить один язык, который на 90% одинаково будет себя вести на разных платформах, чем учить 2 языка. Да и ни Powershell своими командлетами, ни Bash своим синтаксисом мне совершенно не нравятся. Тем более, что у Python есть уже огромное количество разнообразных модулей и вы можете даже сделать для своих скриптом кросплатформенный GUI, если уж совсем скучно станет. В нашей компании есть инженер, который в рамках своих задач по сопровождению приложений и серверов JEE написал не просто скрипт, а целую систему на Bash, которая была положительно отмечена руководством, используется не только в рамках нашей компании и нашими инженерами, но уже и разработчиками самой системы. Так что выбор инструмента, по сути, не так уж и важен. Главное что бы вы были в состоянии реализовать на нём то, что задумываете.

В качестве примера у нас будет несколько скриптов, работающих с коммутаторами Brocade. первый из них — будет генерировать конфиг зон, при добавлении нового устройства в SAN сеть. Задача в принципе простая, достаточно рутинная, но может заниматься много времени ввиду необходимости создания большого кол-ва команд.

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

#!/usr/bin/env python
#! -*- coding: utf-8 -*-

import argparse
from collections import OrderedDict
from datetime import datetime
import os
import paramiko
import re

def connect(host, port, log, pas):
	s = paramiko.SSHClient()
	s.load_system_host_keys()
	s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
	s.connect(host, port, log, pas)
	return s

def findHosts(host, port, log, pas):
	fabHosts = []
	s = connect(host, port, log, pas)
	(stdin, stdout, stderr) = s.exec_command('alishow ESXi*')
	for line in stdout.readlines():
		alias = re.findall('alias:\t(.+)\t\n', line)
		if alias:
			fabHosts.append(alias[0])
	s.close()
	return fabHosts
	
def findCommon(src):
	src.sort(key=len, reverse=True)

	src1 = {}
	for i, s in enumerate(src):
		for j in range(0, len(s)):
			sn = s if not j else s[:-j]
			for k, s2 in enumerate(src[i+1:]):
				for l in range(0, len(s)):
					sn1 = s2 if not l else s2[:-l]
					if sn == sn1:
						if sn not in src1:
							src1[sn] = set()
							src1[sn].add(s)
						src1[sn].add(s2)
						found = True

	src2 = OrderedDict(sorted(src1.items(), key=lambda t: len(t[0]), reverse=True))

	src2_keys = src2.keys()
	for i, k in enumerate(src2_keys):
		if not len(src2[k]):
			src2.pop(k)
		if i == len(src2_keys):
			break
		if k in src2:
			for value in src2[k]:
				is_break = False
				for j, k2 in enumerate(src2_keys[i+1:]):
					if value in src2[k2]:
						if len(src2[k]) == 1:
							src2.pop(k)
							is_break = True
							break
						else:
							src2[k2].remove(value)
				if is_break:
					break

	src2_keys = src2.keys()
	exception = ['_initiator', '_p', '_CL']
	combined = "(" + ")|(".join(exception) + ")"
	for i, k in enumerate(src2_keys):
		result = re.sub(combined, '', k)
		result = re.sub('_$', '', result)
		src2_keys[i] = result
		
	return src2_keys

def findArrays(host, port, log, pas):
	fabArrays = {}
	aliases = []
	exception = ['ESXi*', 'CentOS_TSM_*', 'StdNode1_p*', 'TL_3500_*', 'TRK_MIR_*', 'Zabbix_*']
	combined = "(" + ")|(".join(exception) + ")"
	s = connect(host, port, log, pas)
	(stdin, stdout, stderr) = s.exec_command('alishow *')
	for line in stdout.readlines():
		alias = re.findall('alias:\t(.+)\t\n', line)
		if alias:
			if not re.match(combined, alias[0]):
				aliases.append(alias[0])
	arrays = findCommon(aliases)
	for array in arrays:
		r = re.compile(array + '_.*')
		arrayAliases = filter(r.match, aliases)
		fabArrays.update({array:arrayAliases})
	
	s.close()
	return fabArrays

m = argparse.ArgumentParser(description='Генератор конфигурации зон SAN коммутаторов Brocade в Onlanta Oncloud')
m.add_argument('-type', nargs=1, type=str, help='''Тип добавляемого устройства''')
m.add_argument('-d', nargs=1, type=str, help='''Имя устройства''')
m.add_argument('-a1', nargs=1, type=str, help='''Alias 1''')
m.add_argument('-w1', nargs=1, type=str, help='''WWN 1''')
m.add_argument('-a2', nargs=1, type=str, help='''Alias 2''')
m.add_argument('-w2', nargs=1, type=str, help='''WWN 2''')
m.add_argument('-f', nargs=1, type=int, help='''Номер фабрики''')
m.add_argument('-c', nargs=1, type=str, help='''Имя конфигурации''')
options = m.parse_args()

if options.f[0] == 1:
	host = 'IP'
	login = 'admin'
	password = 'pass'
if options.f[0] == 2:
	host = 'IP'
	login = 'admin'
	password = 'pass'

print 'Generating config start.'
if options.type[0] == 'storage':
	fabric = findHosts(host, 22, login, password)

if options.type[0] == 'server':
	fabric = findArrays(host, 22, login, password)
		
filename = 'config_' + datetime.strftime(datetime.now(), '%Y%m%d%H%M%S') + '.txt'
f = open(filename, 'w')

s = 'alicreate "' + options.a1[0] + '", "' + options.w1[0] + '"'
f.write(s + '\n')

s = 'alicreate "' + options.a2[0] + '", "' + options.w2[0] + '"'
f.write(s + '\n')

if options.type[0] == 'storage':
	for host in fabric:
		s = 'zonecreate "'+ host +'_'+ options.d[0] +'", "'+ host +';' + options.a1[0] + ';' + options.a2[0] + '"'
		f.write(s + '\n')

	s = 'cfgadd "'+ options.c[0] +'", "'
	for host in fabric:
		s += host +'_'+ options.d[0] + ';'
	s += '"'
	f.write(s + '\n')
	
if options.type[0] == 'server':
	for key, value in fabric.items():
		s = 'zonecreate "'+ options.a1[0] +'_'+ key +'", "' + options.a1[0] + ';'
		for alias in value:
			s += alias + ';'
		s += '"'
		f.write(s + '\n')
		
		s = 'zonecreate "'+ options.a2[0] +'_'+ key +'", "' + options.a2[0] + ';'
		for alias in value:
			s += alias + ';'
		s += '"'
		f.write(s + '\n')
	
	s = 'cfgadd "'+ options.c[0] +'", "'
	for key, value in fabric.items():
		s += options.a1[0] +'_'+ key + ';'
		s += options.a2[0] +'_'+ key + ';'
	s += '"'
	f.write(s + '\n')
	
s = 'cfgsave'
f.write(s + '\n')
s = 'cfgenable "'+ options.c[0] +'"'
f.write(s + '\n')

print 'Generating config done.'
print 'Config file name: ' + filename;

Исходник на Pastebin
И так, начнём с главного — что же делает скрипт?
Перед нами стоит задача — добавить в нашу существующую SAN сеть, новое устройство. Это будет или сервер или СХД. Для того, что бы это сделать, в первую очередь нам нужно получить список уже имеющихся устройство в SAN сети. В зависимости от типа добавляемого устройства это будут или серверы или СХД. Конечно, будет очень удобно, если у алиасов в вашем зонинге есть какие-то общие именования, к которым можно привязаться. Вот в плане серверов у нас всё просто ESXi_*, есть ещё несколько отдельный серверов, но для них зонинг существует отдельно, т.к. это не основные, а вспомогательные серверы и не со всеми СХД они связаны зонингом. С СХД же всё намного сложнее, поэтому тут придётся повозиться с фильтрацией + есть очень важная задача, для красивого именования зон не просто взять алиасы как они есть, а найти среди них уникальные части и убрать всё лишнее. Но это я уже покажу на примере, для большей наглядности.

И так, начнём с самого начала и я расскажу о дополнительных модулях, которые обычно использую:
argparse удобная вещь, позволяющая работать с передаваемыми скрипту аргументами. Не всегда я сам помню все ключи всех своих скриптов, поэтому автоматически генерируемый им help крайне полезен.

OrderedDict в данном примере будет использоваться именно для «очеловечивания» алиасов СХД, для красивого именования зонинга.
paramiko модуль, который я использую во всех своих скриптах, который позволяет удобно работать с устройствами по протоколу ssh.
re если вам предстоит делать что-то с ответами на команды от оборудования — без регулярных выражений просто не обойтись.

Функции:
connect — это обёртка для paramiko, для соединения с коммутаторами.
findHosts — функция создания списка алиасов хостов с коммутатора. При помощи функции connect она подключается к коммутатору, выполняет команду alishow ESXi*, затем при помощи регулярных выражений обрабатывает полученный от коммутатора ответ и у нас получается полный список алиасов хостов. Как я уже ранее говорил — хорошо если ваши алиасы имеют какую-то общую часть, что бы это можно было легко описать логикой. Следующая функция по поиску алиасов СХД будет как раз для тех, у кого такой логики нет.
findArrays — аналогичная функция, что и findHosts, только ищет не хосты, а массивы. Как можно видеть, в данном случае у нас есть список исключений (exception) для исключения всего ненужного. Если бы в именования наших массивов присутствовала какая-то логика, к примеру stor_NetApp_8200, stor_fas2650 и т.д., автоматизировать было бы проще. Стоит учитывать подобные вещи, если вы планируете зонинг с нуля. В дальнейшем этим вы сильно упростите себе жизнь.
findCommon — это продолжение функции findArrays. Как я уже говорил, из алиасов вида Storwize_p1, Storwize_p2, FAS_8200_initiator1_0f, FAS_8200_initiator2_0f нам нужно вычленить именование непосредственно массивов, для того, что бы мы имели возможность создавать читабельные имена зон, типа ESXi01_3_NetApp_8200.

И так, логика работы следующая. Вызывая скрипт, вы в параметрах передаёте необходимые данные — тип добавляемого устройства (что бы скрипт понимать — зонинг для чего именно нам делать и какие устройства вытаскивать с коммутаторов), имя (которое будет использоваться при именовании зоны), 2 алиаса, 2 WWNа (мы подключаем и СХД и серверы по 2 портам, за редким исключением. можно сделать и больше, а можно просто дважды запустить скрипт, если вы подключаете СХД по 4 портам), номер фабрики (от этого зависит выбор коммутатора. Деление на 1 и 2 тут условное, но мы помним, что FC фабрик у нас всегда должно быть 2 — для отказоустойчивости) и имя конфигурационного файла на коммутаторе.
Вот так это будет выглядеть

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

Пример добавления СХД.
Пример добавления СХД.
Пример добавления сервера.
Пример добавления сервера.

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

x = str(raw_input("Commit? [Y/N] "))
if x == 'Y' or x == 'y' or x == 'yes' or x == 'Yes':
    s = connect(host, 22, login, password)
    with open(filename) as f:
        content = f.read().splitlines()
        for c in content:
            (stdin, stdout, stderr) = s.exec_command(c)
    print 'Commit done.’
    os.remove(filename)
    s.close()

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

Пример 2: Ищем устройство на порту
Наши коммутаторы мониторятся при при помощи Zabbix, в том числе и ошибки на портах. Я об этом в статьях по Brocade ещё не говорил, это тема лишь 7 и 13 частей, но в данном случае для примере пока не важно, что именно за ошибки на портах у нас есть. В любом случае, для того, что бы начать разбирать в возникшей проблеме — нам необходимо понять с какого устройства всё началось.
er_other_discard port 4 delta (IBM SAN 10.6.220.10:brocadeportstaterrlld.sh[er_other_discard,4,10,1]): 30
Здесь у на сошибка на 4-и порту коммутатора 10.6.220.10
Да, мы можете зайти самостоятельно на коммутатор, сделать switchshow, посмотреть WWN на порту, сделать nodefind. А может быть ещё и пароль от коммутатора у вас сгенерирован и записан где-нибудь в хранилище паролей, пароль от которого ещё нужно вспомнить. В общем да — я ленив, и никогда этого не скрывал 🙂

Пример 3: Карта подключений
Порой для отчётности или для составления документации нам необходимо составить карту подключений устройств к портам коммутатора. Можно записывать их сразу, можно долго сопоставлять алиасы и WWNы устройств. Но это не интересно, проще всё это автоматизировать.

Пример 2 и 3 у меня реализованы в виде единого скрипта (на самом деле он делает ещё несколько функций, но описывать всё просто не вижу смысла) и задачи в общем то одинаковы, только для одной из них нас интересует конкретный порт на конкретном коммутаторе, во втором же случае нас интересуют все порты одного или нескольких коммутаторов фабрики.

Работает это очень просто

Поиск устройства на порту. В данном примере у нас порт транковый
Поиск устройства на порту. В данном примере у нас порт транковый

Поиск всех устройств на коммутаторе
Поиск всех устройств на коммутаторе

Сам скрипт:

#!/usr/bin/env python
#! -*- coding: utf-8 -*-

import argparse
import paramiko
import re
import sys

host = {
	'10': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 12
	},
	'11': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 12
	},
	'12': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 12
	},
	'13': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 12
	},
	'18': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 12
	},
	'19': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 12
	},
	'23': {
		'address' : '1IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 24
	},
	'24': {
		'address' : 'IP',
		'login' : 'admin',
		'password' : 'pass',
		'ports' : 24
	},
}

def connect(host, port, log, pas):
	s = paramiko.SSHClient()
	s.load_system_host_keys()
	s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
	s.connect(host, port, log, pas)
	print '*** Connected to', host
	return s
	
def findali(s, port):
	wwn = findWWN(s, port)
	if wwn == 'Trunk port':
		alias = 'Trunk port'
	else:
		alias = findAlias(s, wwn)
	if alias is None:
		alias = wwn
	print 'port', port, ':', alias, '(', wwn, ')'
	
def findWWN(conn, port):
	command = 'portshow ' + str(port)
	(stdin, stdout, stderr) = s.exec_command(command)
	
	for line in stdout.readlines():
		wwn = re.findall('\t(([0-9A-Fa-f]{2}:?){8})', line)
		if len(wwn) > 0:
			return wwn[0][0]
		trunk = re.findall('Trunk (master )?port', line)
		if len(trunk) > 0:
			return 'Trunk port'	

def findAlias(conn, wwn):
	command = 'nodefind ' + str(wwn)
	(stdin, stdout, stderr) = s.exec_command(command)
	
	for line in stdout.readlines():
		aliases = re.findall('Aliases: (.+)\n', line)
		if aliases:
			return aliases[0]
			
def switchshow(conn):
	command = 'switchshow'
	(stdin, stdout, stderr) = s.exec_command(command)

	for line in stdout.readlines():
		line = line.rstrip()
		if line != '':
			print line
		   
def process():
	if command == 'findali':
		try:
			port = options.p[0]
			if port == 'all':
				for i in range(0, host[switch]['ports']):
					globals()[command](s, i)
			elif ',' in port:
				for i in port:
					if i == ',':
						continue
					globals()[command](s, i)
			else:
				globals()[command](s, port)
		except TypeError:
			print 'Port not defined.';
			exit()

m = argparse.ArgumentParser(description='''Работа с SAN коммутаторами Brocade в Onlanta Oncloud''')
m.add_argument('-c', nargs=1, type=str, help='''Комманда для выполнения. findali''')
m.add_argument('-switchall', action='store_true', help='Выполнение на всех SAN коммутаторах.')
m.add_argument('-a', nargs=1, type=str, help='Адресс SAN коммутатора (указываются последние 2 цифры IP адреса).')
m.add_argument('-dc', nargs=1, type=str, help='Выбор всех коммутаторов ЦОДа (SafeData или IXCellerate).')
m.add_argument('-f', nargs=1, type=str, help='Выбор всех коммутаторов фабрики (1 и 2).')
m.add_argument('-st', nargs=1, type=str, help='Выбор типа коммутаторов фабрик (core и edge).')
m.add_argument('-p', nargs=1, type=str, help='Порт коммутатора. Можно указать . Нумерация портов начинается с 0.')
options = m.parse_args()

if options.a:
	switch = options.a[0]
	s = connect(host[switch]['address'], 22, host[switch]['login'], host[switch]['password'])
	command = options.c[0]
	process()
	s.close()
	print '*** Disconnect...'
	exit(0)
	
if options.switchall:
	collection = ['10', '11', '12', '13','18', '19', '23', '24']

if options.dc:
	datacenter = options.dc[0]
	if (datacenter == 'SafeData'):
		collection = ['18', '19', '23', '24']
	if (datacenter == 'IXCellerate'):
		collection = ['10', '11', '12', '13']

if options.f:
	fabric = options.f[0]
	if (fabric == '1'):
		collection = ['10', '12', '18', '24']
	if (fabric == '2'):
		collection = ['11', '13', '19', '23']

if options.st:
	switchtype = options.st[0]
	if (switchtype == 'core'):
		collection = ['10', '11', '18', '19']
	if (switchtype == 'edge'):
		collection = ['12', '13', '23', '24']
				
for switch in collection:
	s = connect(host[switch]['address'], 22, host[switch]['login'], host[switch]['password'])
	command = options.c[0]
	process()
	s.close()
	print '*** Disconnect...'

Исходник на Pastebin

Набор дополнительных модулей в общем то тот же самый — для работы с параметрами коммандной строки, работа с ssh и регулярные выражения.

И так, в начале нам нужно создать словарь с нашими коммутаторами, где мы указываем их ip, логин, пароль и количество портов. Последний параметр будет важен, если у ваших коммутаторов разное количество портов, и скрипту нужно знать — сколько портов опрашивать, когда в качестве аргумента к порту мы передаём «all».

Функции:
connect — как и в предыдущем скрипте отвечает за подключение к коммутатору при помощи paramiko
findali — собственно эту команду мы и вызываем, она занимается тем, что запускает дву другие функции для поиска WWN на порту и его сопоставление с алиасом и затем выводом информации в консоль.
findWWN — получает курсор подключения к коммутатору и ищет WWN на указанном порту.
findAlias — получает курсор подключения к коммутатору и ищет алиас переданного WWN.

при помощи argparse снова создана менюшка, которая генерирует хелп

И так — параметры:
-c — выполняемая команда. В нашем случае только findali, остальное я убрал, что бы не загромождать код.
-switchall — выполнение команды на всех коммутаторах
-a — выполнение команды на конкретном коммутаторе (в качестве идентификатора я использую последний октет адреса коммутатора)
-dc — выполнение команды на коммутаторах в конкретном ЦОДе
-f — выполнение команды на коммутаторах первой или второй фабрики
-st — выполнение команды на коммутаторах по логическому распределению core/edge
-p — порт. может быть указан как один, так и через запятую, либо all.

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

Надеюсь этим небольшими примерами, я показал как можно сделать свои рабочие будни немножко проще.

 

Добавить комментарий