目录
一、项目介绍
二、项目背景
1.互联网快速发展
2.云时代
3.项目准备
1.运维自动化难点和痛点
2.项目技术难点
3.项目功能演示
三.整体工程设计
1.资产的自动化扫描发现
2.Ansible的自动化任务执行
四、项目环境搭建
1.项目环境要求
2.项目目录的配置
五、远程服务器虚拟环境的配置
六、MySQL数据库配置
1.远程用户登录配置
2.Django数据库配置
七、第一个DevOPS工程
1.项目功能
2.项目开发步骤
3.项目开发(一) 信息采集接口的实现
4.项目开发(二) 信息获取接口的实现
5.Django项目日志管理
6.Django项目邮件告警管理
7.知识点拓展: Ipython
8.常用快捷操作
八、第二个DevOPS工程
1.Django工程多配置文件
2.Django工程应用与模块加载
3.数据库模型设计
4.资产管理的技术实现
5.资产管理探测流程
6.主机存活探测协议
7.主机存活探测模块和工具
8.Nmap的Python操作接口: python-nmap
9.SSH端口存活扫描
九、Python的SSH登录模块paramiko
cd /data/www/devops
virtualenv -p /usr/bin/python3 env
source env/bin/active
pip install Django==2.2
# sqlite版本问题
django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is
required (found 3.7.17).
解决方式: 不使用sqlite存储数据, 使用mysql
# devops/settings.py
# 将sqlite数据库存储配置注释
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
# }
python manage.py runserver
systemctl start mariadb
systemctl enable mariadb
$ python manage.py startapp scanhosts
# settings.py
# 1). 将新建的APP加入到项目中
INSTALLED_APPS = [
......省略部分
'django.contrib.staticfiles',
'scanhosts',
]
# 2). 配置数据库: 使用mysql数据库,而不是默认的sqlite数据库。
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
# }
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'devopsProject', # 数据库名称
'USER': 'devops', # 用户名
'PASSWORD': 'westos', # 用户密码
'HOST': '127.0.0.1', # 数据库服务器所在主机名
'PORT': '3306', # 数据库端口
}
}
# 3). 语言和时区的设置(根据自己项目的需求, 选择修改)
# LANGUAGE_CODE = 'en-us'
# TIME_ZONE = 'UTC'
LANGUAGE_CODE = 'zh-hans' # 语言选择中文
TIME_ZONE = 'Asia/Shanghai' # 时区选择亚洲/上海
$ yum install mariadb-devel -y
$ pip install mysqlclient
# models.py
"""
- 一个类对应一个数据库表;
- 类的一个属性对应数据库表的一个表头;
- max_length: 字符串最大长度, 对应数据库的varchar类型
- default: 指定默认值
- verbose_name: 指定Django后台显示的列头信息
- auto_now: 每次修改记录时自动更新为当前时间
- Meta类的设置
- verbose_name: 指定Django后台显示的表名称单数
- verbose_name_plural: 指定Django后台显示的表名称复数
- db_table: 指定数据库表的名称, 默认是APP名称_类名称.
"""
class UserIPInfo(models.Model):
ip = models.CharField(max_length=150, default='', verbose_name='IP地址')
time = models.DateTimeField(verbose_name='更新时间', auto_now=True)
class Meta:
verbose_name = '用户访问地址信息表'
verbose_name_plural = verbose_name
db_table = 'user_IP_info'
class BrowseInfo(models.Model):
# null=True: 是针对数据库而言,True表示数据库的该字段可以为空。
user_agent = models.CharField(max_length=100, default='',
verbose_name='用户浏览器信息', null=True)
disk_id = models.CharField(max_length=256, default='', verbose_name='唯
一设备ID')
"""
ForeignKey是一种关联字段,将两张表进行关联的方式
on_delete: 是否级联删除, Django1.x默认级联删除, Django2.x必须手动指定
on_delete有CASCADE、PROTECT、SET_NULL、SET_DEFAULT、SET()五个可选择的值
CASCADE:此值设置,是级联删除。
PROTECT:此值设置,是会报完整性错误。
SET_NULL:此值设置,会把外键设置为null,前提是允许为null。
SET_DEFAULT:此值设置,会把设置为外键的默认值。
SET():此值设置,会调用外面的值,可以是一个函数。
"""
user_ip = models.ForeignKey('UserIPInfo', on_delete=models.DO_NOTHING)
class Meta:
verbose_name = '用户浏览器信息表'
verbose_name_plural = verbose_name
db_table = 'browse_info'
$ python manage.py makemigrations
# 代码执行效果, 生成迁移文件,所在位置: scanhosts/migrations
Migrations for 'scanhosts':
scanhosts/migrations/0001_initial.py
- Create model UserIPInf
$ python manage.py migrate
$ python manage.py createsuperuser
Username (leave blank to use 'kiosk'): admin
Email address: [email protected]
Password:
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
# first_devops/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
url(r'^sendinfos/$', user_infos),
]
# scanhosts/views.py
def user_info(request):
# request.META 是一个Python字典,包含了所有本次HTTP请求的Header信息,比如用户IP
地址和用户Agent(通常是浏览器的名称和版本号)
ip = request.META.get('REMOTE_ADDR')
user_agent = request.META.get('HTTP_USER_AGENT')
# 使用filter()方法对数据进行过滤, 返回的是列表, 列表元素是符合条件的对象。
user_obj = UserIPInfo.objects.filter(ip=ip)
# 如果没有找到,则新建UserIPInfo对象,并获取对象编号(为了和BrowseInfo表关联)
if not user_obj:
res = UserIPInfo.objects.create(ip=ip)
user_ip_id = res.id
else:
user_ip_id = user_obj[0].id
# 新建BrowseInfo对象
BrowseInfo.objects.create(user_agent=user_agent, user_ip_id=user_ip_id)
# 字典封装返回的数据信息
result = {
'STATUS': 'success',
'INFO': 'User Info',
'IP': ip,
'User-Agent': user_agent
}
# 以json的方式封装返回, 下面的两种方式任选一种.
# return HttpResponse(json.dumps(result),
content_type='application/json')
return JsonResponse(result)
# first_devops/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
url(r'^sendinfos/$', user_info),
url(r'^getinfos/$', user_history),
]
# scanhosts/views.py
def user_history(request):
# 获取UserIPInfo表的所有对象信息;
ip_lists = UserIPInfo.objects.all()
infos = {}
# 获取每个IP访问网站浏览器的信息, 格式如下:
"""
infos = {
'127.0.0.1' : ['UA-1', 'ua-2'],
'172.25.254.1' : ['UA-1', 'ua-2'],
}
"""
for item in ip_lists:
infos[item.ip] = [b_obj.user_agent for b_obj in
BrowseInfo.objects.filter(user_ip_id=item.id)]
result = {
'STATUS': 'success',
'INFO': infos
}
return JsonResponse(result)
# first_devops/settings.py
# 日志管理的配置
LOGGING = {
'version': 1,
# disable_existing_loggers是否禁用已经存在的logger实例。默认值是True.
'disable_existing_loggers': False,
# formatters: 定义输出的日志格式。
'formatters': {
'verbose': {
# 格式化属性查看资料:
https://docs.python.org/3/library/logging.html#logrecord-attributes
'format': '{levelname} {asctime} {module} : {lineno} {message}',
# 变量的风格
'style': '{',
# 日期显示格式
'datefmt': '%Y-%m-%d %H:%M:%S',
},
},
# handlers定义处理器。
'handlers': {
'file': {
# 日志处理级别
'level': 'INFO',
# 日志处理类, 详细的请查看网站:
https://docs.python.org/3/library/logging.handlers.html
'class': 'logging.FileHandler',
# 日志处理后输出格式
'formatter': 'verbose',
# 记录日志的文件名, 存储在当前项目目录下的devops.log文件
'filename': os.path.join(BASE_DIR, 'devops.log')
},
},
# loggers定义logger实例。
'loggers': {
'django': {
# 对应的handles对象列表
'handlers': ['file'],
# logger实例输出的日志级别
'level': 'INFO',
# 日志是否向上级传递。True 向上级传,False 不向上级传。
'propagate': True,
},
}
}
# first_devops/settings.py
# 邮件配置
EMAIL_HOST = 'smtp.qq.com'
EMAIL_HOST_USER = 'QQ邮箱'
EMAIL_HOST_PASSWORD = '登录授权码(注意: 不是登录密码)'
EMAIL_PORT = 465
EMAIL_SUBJECT_PREFIX = 'Python开发社区'
EMAIL_USE_SSL = True
$ python manage.py shell
>>> from django.core.mail import send_mail
>>> help(send_mail)
>>> send_mail(subject="Django邮件发送测试代码", message='邮件发送测试成功',
from_email='发送人的邮箱地址', recipient_list=['接收人的邮箱地址1', '接收人的邮箱地
址2'])
1
# first_devops/scanhosts/utils/tools.py
import logging
from django.core.mail import send_mail
from datetime import datetime
from first_devops import settings
class SendMail(object):
"""发送邮件的封装类"""
def __init__(self, subject, message, recipient_list, ):
# 给每个邮件的标题加上当前时间, 时间格式为年月日_小时分钟秒_传入的邮件标题
subject_time = datetime.now().strftime('%Y%m%d_%H%M%S_')
self.recipient_list = recipient_list
self.subject = subject_time + subject
self.message = message
def send(self):
try:
send_mail(
subject=self.subject,
message=self.message,
from_email=settings.EMAIL_HOST_USER,
recipient_list=self.recipient_list,
fail_silently=False
)
return True
except Exception as e:
logging.error(str(e))
return False
$ python manage.py shell
>>> from scanhosts.utils.tools import SendMail
>>> mail = SendMail('Django 测试标题', '邮件正文内容', ['[email protected]'])
>>> mail.send()
True
$ pip install ipython
$ ipython
Python 3.7.5 (default, Oct 25 2019, 15:51:11)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.10.2 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
from .base import *
DEBUG = True
from .base import *
# 开发环境一定要关闭调试模式
DEBUG = False
# 允许所有主机访问
ALLOWED_HOSTS = ['*']
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'devops.settings.development')
# ......此处省略代码
if __name__ == '__main__':
main()
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'devops.settings.production')
# ......此处省略代码
if __name__ == '__main__':
main()
$ python manage.py runserver
Django version 2.2.5, using settings 'devops.settings.development'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 将apps目录和extra_apps添加到python的搜索模块的路径集中
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
sys.path.insert(0, os.path.join(BASE_DIR, 'extra_apps'))
# .........
$ python manage.py shell
In [1]: import sys
In [2]: sys.path
Out[2]:
['/home/kiosk/PycharmProjects/devops/devops/extra_apps',
'/home/kiosk/PycharmProjects/devops/devops/apps',
'/home/kiosk/PycharmProjects/devops',
# .......此处为了美观, 省略部分路径........
]
# 使用-sP开关(Arp ping)执行PING命令,与windows / linux ping命令类似。
$ ping -c1 -w1 172.25.254.197
$ nmap -n -sP 172.25.254.197
$ nmap -n -sP 172.25.254.0/24
# Nmap默认端口的扫描范围1-10000
$ nmap -n -p 172.25.254.197
# 具体指定要扫描的端口为50-80
$ nmap -n -p50-80 172.25.254.197
# 具体指定要扫描的端口为22和80
$ nmap -n -p22,80 172.25.254.197
# 安装nmap的第三方模块
$ pip install python-nmap
具体的代码调用如下:
import nmap
# 实例化对象, portScanner()类用于实现对指定主机进行端口扫描
nm = nmap.PortScanner()
# 以指定方式扫描指定主机或网段的指定端口
result = nm.scan(hosts='172.25.254.0/24', arguments='-n -sP')
print("扫描结果: ", result)
# 返回的扫描具体的nmap命令行
print("nmap命令行: ", nm.command_line())
# 返回nmap扫描的主机清单,格式为列表类型
print("主机清单: ", nm.all_hosts())
# 查看指定主机信息
print('172.25.254.197的主机信息: ', nm['172.25.254.197'])
扫描结果:
{'nmap': {'command_line': 'nmap -oX - -n -sP 172.25.254.0/24', 'scaninfo': {},
'scanstats': {'timestr': 'Wed Dec 25 16:14:47 2019', 'elapsed': '6.06',
'uphosts': '2', 'downhosts': '254', 'totalhosts': '256'}}, 'scan':
{'172.25.254.197': {'hostnames': [{'name': '', 'type': ''}], 'addresses':
{'ipv4': '172.25.254.197'}, 'vendor': {}, 'status': {'state': 'up', 'reason':
'syn-ack'}}, '172.25.254.250': {'hostnames': [{'name': '', 'type': ''}],
'addresses': {'ipv4': '172.25.254.250'}, 'vendor': {}, 'status': {'state': 'up',
'reason': 'syn-ack'}}}}
nmap命令行: nmap -oX - -n -sP 172.25.254.0/24
主机清单: ['172.25.254.197', '172.25.254.250']
172.25.254.197的主机信息:
{'hostnames': [{'name': '', 'type': ''}], 'addresses': {'ipv4':
'172.25.254.197'}, 'vendor': {}, 'status': {'state': 'up', 'reason': 'syn-ack'}}
telnetlib模块提供的Telnet类实现了Telnet协议。
import telnetlib
import re
# 实例化对象
tn = telnetlib.Telnet(host='172.25.254.197', port=22, timeout=5)
# read_until读取直到遇到了换行符或超时秒数。默认返回bytes类型,通过decode方法解码为字符
串。
tn_result = tn.read_until(b"\n", timeout=5).decode('utf-8')
# 通过正则匹配且忽略大小写, 寻找是否ssh服务开启。
ssh_result = re.search(pattern=r'ssh', string=tn_result, flags=re.I)
# 如果能匹配到内容, 说明ssh服务开启, 是Linux服务器.
if ssh_result:
print("Linux服务器存活")
else:
print("其他服务器存活")
Linux服务器存活
扫描探测小结
import pexpect
cmd_result, exitstatus = pexpect.run('hostname', withexitstatus=True)
print("命令执行结果: ", cmd_result.decode('utf-8'))
print("命令执行的状态码: ", exitstatus)
执行结果如下:
命令执行结果: foundation0.ilt.example.com
命令执行的状态码: 0
import pexpect
# logging:专门做日志记录或者处理的模块。
import logging
# 日志的基本配置
# 官方符网站基本配置及使用: https://docs.python.org/zh-
cn/3/howto/logging.html#logging-basic-tutorial
logging.basicConfig(
level=logging.DEBUG, # 控制台打印的日志级别
filename='message.log', # 日志文件位置
filemode='a', # 写入文件的模式,a是追加模式,默认是追加模式
# 日志格式
format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %
(message)s'
)
class SSH(object):
def login_ssh_passwd(self, hostname, username, password, port=22):
"""
用于pexpect实现ssh自动化用户密码登录
"""
print("ssh -p%s %s@%s" % (port, username, hostname))
if port and username and password:
ssh = pexpect.spawn("ssh -p%s %s@%s" % (port, username, hostname))
status_code = ssh.expect(['password', 'continue connecting
(yes/no)?'], timeout=5)
if status_code == 0:
ssh.sendline(password)
elif status_code == 1:
ssh.sendline('yes\n')
ssh.expect('password: ')
ssh.sendline(password)
index = ssh.expect(["#", pexpect.EOF, pexpect.TIMEOUT])
if index == 0:
print("logging in as root!")
logging.info("logging in as root!")
ssh.interact()
elif index == 1:
print("logging process exit!")
logging.error("logging process exit!")
elif index == 2:
print("logging timeout exit!")
logging.error("logging timeout exit!")
else:
print("Parameter error!")
logging.error("Paramiko error!")
def login_ssh_key(self, hostname, keyfile, username, port=22):
"""
用于实现pexpect实现ssh的自动化密钥管理
"""
print("ssh -i %s -p%s %s@%s" % (keyfile, port, username, hostname))
if hostname and keyfile and username and port:
ssh = pexpect.spawn("ssh -i %s -p%s %s@%s" % (keyfile, port,
username, hostname))
status_code = ssh.expect([pexpect.TIMEOUT, 'continue connecting
(yes/no)?'], timeout=5)
if status_code == 1:
ssh.sendline('yes\n')
index = ssh.expect(["#", pexpect.EOF, pexpect.TIMEOUT])
if index == 0:
print("logging in as root!")
logging.info("logging in as root!")
# 进入交互式环境
ssh.interact()
elif index == 1:
print("logging process exit!")
logging.error("logging process exit!")
elif index == 2:
print("logging timeout exit!")
logging.error("logging timeout exit!")
else:
print("Paramiko error!")
logging.error("Parameter error!")
def main():
sshClient = SSH()
# sshClient.login_ssh_passwd('172.25.254.197', 'root', 'redhat')
# 必须做过无密码连接
sshClient.login_ssh_key('172.25.254.197','/root/.ssh/id_rsa', 'root', )
if __name__ == '__main__':
main()
# 使用豆瓣的镜像源, 安装paramiko模块并指定安装版本为2.6.0.
$ pip install -i https://pypi.douban.com/simple paramiko==2.6.0
import paramiko
def login_ssh_passwd(hostname='172.25.254.197', port=22,
username='root', password='westos', command='df -h'):
"""
基于paramiko实现ssh自动化用户密码登录
:return:
"""
# 实例化SSHClient
with paramiko.SSHClient() as client:
# 自动添加策略,保存服务器的主机名和密钥信息,如果不添加,那么不再本地know_hosts文件中记录的主机将无法连接
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接SSH服务端,以用户名和密码进行认证
client.connect(hostname=hostname, port=port, username=username,
password=password)
# 打开一个Channel并执行命令。 stdout 为正确输出,stderr为错误输出,同时是有1
个变量有值
stdin, stdout, stderr = client.exec_command(command)
# 打印执行结果
print(stdout.read().decode('utf-8'))
if __name__ == '__main__':
login_ssh_passwd(hostname='127.0.0.1', command='hostname')
foundation0.ilt.example.com
def login_ssh_key( hostname='172.25.254.197', keyfile='./id_rsa',
username='root', port=22, command='df -h'):
"""
基于paramiko实现ssh的自动化密钥管理
"""
# 配置私人密钥文件位置
private = paramiko.RSAKey.from_private_key_file(keyfile)
# 实例化SSHClient
with paramiko.SSHClient() as client:
# 自动添加策略,保存服务器的主机名和密钥信息,如果不添加,那么不再本地
know_hosts文件中记录的主机将无法连接
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接SSH服务端,以用户名和密码进行认证
client.connect(hostname=hostname, port=port, username=username,
pkey=private)
# 打开一个Channel并执行命令。 stdout 为正确输出,stderr为错误输出,同时是有1
个变量有值
stdin, stdout, stderr = client.exec_command(command)
# 打印执行结果
print(stdout.read().decode('utf-8'))
if __name__ == '__main__':
# login_ssh_passwd(hostname='127.0.0.1', command='hostname')
login_ssh_key(hostname='127.0.0.1', command='hostname')
运行效果如下:
foundation0.ilt.example.com
from_transport(cls,t) 创建一个已连通的SFTP
客户端通道
put(localpath, remotepath, callback=None, confirm=True) 将本地文件上传到服务
器 参数confirm: 是否调用stat()方法检查文件状态,返
回ls -l的结果
get(remotepath, localpath, callback=None) 从服务器下载文件到本
地
mkdir() 在服务器上创建目录
remove() 在服务器上删除目录
rename() 在服务器上重命名目录
stat() 查看服务器文件状态
listdir() 列出服务器目录下的文件
import paramiko
# 获取Transport实例
with paramiko.Transport(('172.25.254.197', 22)) as tran:
# 连接SSH服务端,使用password
tran.connect(username="root", password='westos')
# # 或使用
# # 配置私人密钥文件位置
# private = paramiko.RSAKey.from_private_key_file('./id_rsa')
# # 连接SSH服务端,使用pkey指定私钥
# tran.connect(username="root", pkey=private)
# 获取SFTP实例
sftp = paramiko.SFTPClient.from_transport(tran)
# 设置上传的本地/远程文件路径
localpath = "/etc/passwd"
remotepath = "/mnt/passwd"
# 执行上传动作
sftp.put(localpath, remotepath)
# # 执行下载动作
# sftp.get(remotepath, localpath)