项目实战:CMDB自动化资产扫描和自动化运维

1、项目介绍

本项目基于Linux系统搭建系统集群,使用Ansible实现Linux集群下的批量部署和自动化管理,实现Web形式的自动化运维系统,集中批量控制服务器,最终实现能支撑1000台实例的环境提供管理和自动化任务,提高运维工程师的工作效率和质量。项目基于HTTP实现自动化任务接受和响应接口设计,基于MySQL用作的关系型数据存取,基于Redis的任务锁机制和消息队列, 基于MongoDB的事件日志记录, 最终实现邮件通知功能、敏感数据加密功能、日志事件记录功能。

主要目标是实现自动化资产扫描, 扫描指定网段的服务器资产信息,后续功能会进一步完善。

2、项目技术分析

在这里插入图片描述

运维自动化难点和痛点

  • 开发人员: 没有系统管理、网络管理等相关运维工作经验,项目设计往往是大打折扣的。
  • 运维人员: 不具备开发能力、没有项目的开发经验或能力
  • 做好一个优秀的运维开发人员DevOPS = 运维能力 + 开发能力

项目技术难点

  • 基本技能
    • DevOPS构建之路
    • Python基础语法
    • Django框架
  • 自动化资产扫描发现
    • 资产扫描的作用
    • nmap的作用
    • telnetlib端口扫描
    • pexpect登录探测
    • paramiko登录探测
    • Docker容器扫描
    • KVM虚拟机扫描
    • snmp网络设备扫描
    • SDK调用扫描ESXI资产信息
  • Ansible自动化任务
    • Ansible的安装与配置
    • Python与Ansible的操作
    • Ansible adhoc
    • Ansible playbook
    • 核心类调用
    • API 接口的封装
    • 方法的改写
    • Redis消息存储
    • Mongo事件日志

整体工程设计

  • 资产的自动化扫描发现
    用Python程序扫描发现企业内部的所有资产(服务器资产),当资产出现变动时能自动及时的发现并完成资产变更(eg: 服务器IP变更、机器集体报废)。
  • Ansible的自动化任务执行
    用Ansible的ad-hoc和playbook实现批量主机的自动化任务。

3、项目环境搭建

项目环境要求

  • Python解释器:3.x
  • Django框架:2.x
  • IDE编辑器工具Pycharm:不限制
  • 自动化运维工具Ansible:2.x
  • 关系型数据库MySQL/Mariadb:5.5.x
  • Git工具与Git代码仓库:不限制

项目环境的搭建

项目目录的配置
  • 创建Django项目devops
    在这里插入图片描述

如果有云服务器时执行下面的操作,没有云服务器的操作在这个之后进行说明

  • 连接并配置远程服务器
    [tools]>[Deployment]
    在这里插入图片描述

    • 配置本地目录和远程服务器目录的映射(Mapping)

    在这里插入图片描述
    上述操作完成,修改本地文件,远程服务器文件也同时被修改。

远程服务器虚拟环境的配置
  • 连接远程服务器命令行bash
    在这里插入图片描述
  • 创建虚拟环境并激活虚拟环境
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

# CMDB/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'devopsProject',
        'USER': 'devops',
        'PASSWORD': '',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}
  • 远程服务器解释器和目录映射的配置

在这里插入图片描述

  • 在远程服务器上测试Django项目是否运行成功
python manage.py runserver

如果没有云服务器直接进行虚拟环境的创建
执行下面命令:

witch python3  # /usr/bin/python3
virtualenv -p /usr/bin/python3 env     #创建虚拟环境
source env/bin/active		#切换到虚拟环境
pip install Django==2.2 

MySQL数据库配置

(远程)用户登录配置
  • 管理数据库服务
[root@foundation0 ~]# systemctl start mariadb
[root@foundation0 ~]# systemctl enable mariadb
  • 用户授权
[root@foundation0 ~]# mysql -uroot -pServer version: 5.5.52-MariaDB MariaDB Server
# 创建数据库
MariaDB [(none)]> create database if not exists devopsProject default charset
utf8;
Query OK, 1 row affected (0.01 sec)
# 新建用户
MariaDB [(none)]> create user devops@'%' identified by 'westos';
Query OK, 0 rows affected (0.03 sec)
# 用户授权
MariaDB [(none)]> grant all on devopsProject.* to devops@'%';
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> Bye
  • 测试用户授权是否成功
[root@foundation0 ~]# mysql -udevops -pwestos -hIP
Server version: 5.5.52-MariaDB MariaDB Server
MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| devopsProject    
|
+--------------------+
2 rows in set (0.01 sec)
Django数据库配置
# CMDB/settings.py
ALLOWED_HOSTS = ['*']
#
配置数据库: 使用mysql数据库,而不是默认的sqlite数据库。
DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.mysql',
       'NAME': 'devopsProject',
       'USER': 'devops',
       'PASSWORD': 'devops',
       'HOST': '47.92.255.98',
       'PORT': '3306',
 
}
}
# 语言和时区的设置
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
  • 生成数据库表
python manage.py makemigrations  # 生成迁移脚本
python manage.py migrate
# 写入数据库, 创建关于用户和用户组等数据表信息
python manage.py createsuperuser # 创建超级用户
# 启动项目, 访问网址http://IP:8000/admin
python manage.py runserver 0.0.0.0:8000
  • 测试数据表是否创建?数据信息是否写入?
    连接mariadb数据库
    在这里插入图片描述

配置数据库信息
在这里插入图片描述
如果是本地数据库,Host填写127.0.0.1即可

访问数据库表和数据内容
在这里插入图片描述

数据库报错处理

运行项目时,出现报错如下,是因为缺少mysqlclient安装包.
在这里插入图片描述
解决方法:

pip install mysqlclient

安装失败,报错如下是缺少开发包的缘故。
在这里插入图片描述

解决方法

yum install mariab-devel -y
yum install python-devel -y

4、第一个DevOPS工程

项目功能

记录HTTP访问的IP及用户UA信息

  • 运维模块: 了解运维的工作、Linux系统的基本操作、数据库基本管理操作、网络知识等。
  • 开发模块: 本项目的重点, 掌握Python基础知识、常见数据类型、Django框架的技术模块、DevOPS项目构建模块等。

项目开发步骤

  • 创建Django工程
  • 创建Django APP应用
$ python manage.py startapp scanhosts
  • 文件配置settings
# 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' # 时区选择亚洲/上海
  • 数据库模型建模models
    在这里插入图片描述

安装数据库开发软件

$ yum install mariadb-devel -y

安装数据库连接模块(必须先安装mariadb-devel, 否则会报错)

$ 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'

根据ORM(对象关系映射)将面向对象形式的模型进行迁移, 生成中间代码

$ python manage.py
makemigrations
# 代码执行效果, 生成迁移文件,所在位置: scanhosts/migrations
Migrations for 'scanhosts':
scanhosts/migrations/0001_initial.py
   - Create model UserIPInfo

将生成的迁移文件转成SQL语句并执行SQL语句, 创建对应的数据库及数据库表

$ python manage.py migrate
  • Django后台管理界面
    创建后台管理的超级用户
$ python manage.py
createsuperuser
Username (leave blank to use 'kiosk'): admin
Email address: admin@qq.com
Password:
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

启动Django项目,默认开启的端口是8000

$ python manage.py runserver
Django version 2.2.5, using settings 'first_devops.settings'
Starting development server at http://127.0.0.1:8000/

访问项目后台管理界面, 输入超级用户名称和密码即可进入后台界面.
在这里插入图片描述

  • 项目工程设计
    当用户发起HTTP请求时, Django的采集接口将HTTP请求的头部信息headers里面的IP和UA信息采集, 并存储到数据库中。 当用户想要访问采集数据时, 从数据库中读取,以界面的方式展示给用户。
    在这里插入图片描述
  • 项目开发
项目开发(一) 信息采集接口的实现
  • url设计
    配置URL, 当用户访问http://127.0.0.1:8000/sendinfos这个网址时, 将用户请求交给user_info视图函数处理。
# 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)
  • 浏览器访问效果图
    在这里插入图片描述
    浏览器访问结束后, 访问MySQL数据库, 看是否将数据信息采集成功并通过ORM的方式写入数据库中。
[root@foundation0 ~]# mysql -udevops -p
Welcome to the MariaDB monitor.
Commands end with ; or \g.
MariaDB [(none)]> use devopsProject;
MariaDB [devopsProject]> select * from user_IP_info;
MariaDB [devopsProject]> select * from browse_info;
项目开发(二)信息获取接口的实现
  • url设计
    配置URL, 当用户访问http://127.0.0.1:8000/getinfos这个网址时, 将用户请求交给user_history视图函数处理。
# 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)

Django项目日志管理

在编写程序过程中,很难免的会出现一些问题,程序并非按照我们预想的那样运行,这个时候我们通常会对程序进行调试,来看看到底是哪边出了问题。而程序日志是来帮助我们记录程序运行过程的帮手,善用日志的程序员也就能很快找出自己程序的问题所在从而快速解决问题。

在服务器级别的组件中都有对应的日志文件,例如MySQL、Redis、nginx、uWSGI都会在运行过程中将一些信息写到日志文件中。

Django使用python的内置模块logging来管理自己的日志, 包含四大组件: 日志记录器Loggers、日志处理器Handlers、日志过滤器Filters和日志格式化工具Formatters。
在这里插入图片描述
Django项目日志管理详情查看官方文档:https://docs.djangoproject.com/en/2.2/topics/logging/

  • 配置日志的相关信息
# 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,
     
 
},
}
}
  • 写入日志
    修改视图函数的逻辑内容, 在合适的位置添加日志输出, 便于程序的测试与排错。
    在这里插入图片描述
  • 重新访问网页, 查看devops.log文件测试日志是否成功写入
    在这里插入图片描述

Django项目邮件告警管理

在web应用中,服务器对客户发送邮件来通知用户一些信息,可以使用邮件来实现。Django中提供了邮件接口,使我们可以快捷的建设一个邮件发送系统。通常用于发送自定义消息或者通知告警等信息(当然
也可以通过短信接口或者微信接口, 便于维护人员快速响应)。

在这里插入图片描述

  • 服务器端开启smtp协议支持(此处以QQ邮件服务器为例服务器)
    在这里插入图片描述
  • 客户端配置settings文件
# 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
  • 在Django的交互式环境测试邮件是否发送成功
$ 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
  • 查看邮箱是否收到测试邮件

发送邮件在很多业务场景都会适用, 为了方便操作, 将邮件发送的内容封装成一个工具, 减少开发过程中的重复操作, 提高效率。 操作如下:

  • 将项目常用的功能封装到utils模块中, 创建的项目结构,如下:
    在这里插入图片描述
  • 编写封装类SendMail的类
# 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
  • 在Django自带的交互式环境shell‘中进行测试
$ python manage.py
shell
>>> from scanhosts.utils.tools import SendMail
>>> mail = SendMail('Django 测试标题', '邮件正文内容', ['976131979@qq.com'])
>>> mail.send()
True

5、第二个DevOPS工程

在这里插入图片描述

  • 使用Pycharm编辑器工具创建devops项目。

Django工程多配置文件

在这里插入图片描述

  • base.py文件: 基本的配置文件,将原先seetings.py文件的内容拷贝进来.(参考第一个devops项目)
    • 配置数据库
    • 配置时区和语言
  • development.py文件: 开发环境的配置文件
from .base import  *
DEBUG = True
  • production.py文件: 生产环境的配置文件
from .base import  *
# 开发环境一定要关闭调试模式
DEBUG = False
# 允许所有主机访问
ALLOWED_HOSTS = ['*']
  • 修改manage.py文件, 默认寻找的设置文件是当前项目中的settings文件, 如果是开发环境, 修改如下:
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.

Django工程应用与模块加载

为了方便在一个大的Django项目中,管理实现不同的业务功能, 我们会在项目中创建多个APP实现功能。为了更加方便管理APP, 项目结构更加清晰。可以专门创建apps目录存储项目应用, 专门创建extra_apps存储项目第三方APP, 项目结构如下所示:
在这里插入图片描述
但项目运行时, 不会自动寻找apps和extra_apps子目录中创建的APP, 需要手动在配置文件中配置,修改devops/settings/base.py文件, 添加内容如下:

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'))

添加成功后, 进入Django工程的交互式环境, 查看sys.path变量的值, 判断是否添加成功?

$ 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',
# .......此处为了美观, 省略部分路径........
]

资产管理

为什么优先实现资产管理?
  • 资产管理是运维的基本工作;
    在这里插入图片描述
  • 资产管理是DevOPS系统的基础;
    资产管理是自动化运维平台构建的基础。
    在这里插入图片描述

资产管理探测流程

在这里插入图片描述

主机存活探测协议

  • ICMP(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
  • 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
    在这里插入图片描述

主机存活探测模块和工具

Nmap探测工具

Nmap,也就是Network Mapper,最早是Linux下的网络扫描和嗅探工具包。是一款用于网络发现和安全审计的网络安全工具。

  • 主机发现 - 识别网络上的主机。例如,列出响应TCP和/或ICMP请求或打开特定端口的主机。
# 使用-sP开关(Arp ping)执行PING命令,与windows / linux ping命令类似。
$ ping -c1 -w1 172.25.254.197
# 探测主机IP是否存活,
&>/dev/null并将所有的输入重定向到垃圾箱
# && 如果前面的指令执行成功, 做什么操作(echo ok)
# || 如果前面的指令执行失败, 做什么操作(echo fail)
$ ping -c1 -w1 172.25.254.250   &>/dev/null && echo ok || echo fail
# 使用nmap命令, 如果报错-bash: nmap: command not found, 则yum 安装nmap安装包
$ 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
  • 版本检测 - 询问远程设备上的网络服务以确定应用程序名称和版本号。
  • OS检测 - 确定网络设备的操作系统和硬件特性。
# -O是检测操作系统交换机
$ nmap -O 172.25.254.197
  • 可与脚本进行脚本交互 - 使用Nmap脚本引擎(NSE)和Lua编程语言。

查看172.25.254.197这台 主机是否开启?
在这里插入图片描述查看172.25.254.0/24局域网内存活的主机信息及存活主机个数。
在这里插入图片描述

Nmap的Python操作接口:python-nmap

python-nmap是一个使用nmap进行端口扫描的python库,它可以很轻易的生成nmap扫描报告,并且可以帮助系统管理员进行自动化扫描任务和生成报告。同时,它也持nmap脚本输出。

# 安装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'}}

SSH端口存活扫描

  • 使用telnet命令探测主机列表是否属于Linux服务器。
    在这里插入图片描述
  • telnet命令探测
$ telnet 172.25.254.34 22

在这里插入图片描述

  • telnetlib模块探测
    telnetlib模块提供的Telnet类实现了Telnet协议。
# 实例化对象
tn = telnetlib.Telnet(host='172.25.254.34', port=22)
# read_until读取直到遇到了换行符或超时秒数。默认返回bytes类型,通过decode方法解码为字符串。
result = tn.read_until(b'\n', timeout=5).decode('utf-8')
# 通过正则匹配且忽略大小写, 寻找是否ssh服务开启。
searchObj = re.search('ssh', result, re.I)
# 如果能匹配到内容, 说明ssh服务开启, 是Linux服务器.
if searchObj:
    print("ssh服务是开启的,且是Linux操作系统")
else:
    print('ssh服务未开启或者不是Linux服务器')

扫描探测小结

在这里插入图片描述

主机登录探测

什么是主机登录探测?

用一系列的验证方式循环进行SSH登录, 得到争取的登录方式。

主机SSH登录验证方式

SSH常用来远程登录到远程机器,有两种常用的方法

  • 第一种便是账号密码登录。
  • 第二种就是公钥私钥无密码登录。(如何实现无密码登录?)
Python的SSH登录模块pexpect

Pexpect 用来实现与 ssh、ftp 、telnet 等程序的自动交互。是 Expect 语言的一个 Python 实现,是一个用来启动子程序,并使用正则表达式对程序输出做出特定响应,以此实现与其自动交互的 Python模块。

pexpect的核心类和函数。

  • 直接进程运行run()函数, 返回结果和状态。
import pexpect
cmd_result, exitstatus = pexpect.run('hostname', withexitstatus=True)
print("命令执行结果: ", cmd_result.decode('utf-8'))
print("命令执行的状态码: ", exitstatus)

执行结果如下:

命令执行结果:foundation0.ilt.example.com
命令执行的状态码:  0
  • pexpect指令执行的两种方式—无交互和交互式
import pexpect

# 1.通过pexpect执行指令(无交互)
# 执行命令并返回命令执行结果和状态码(0代表成功,其他-执行失败)
(command_output, exitstatus) = pexpect.run('hostname', withexitstatus=1)
command_output = command_output.decode('utf-8')
if exitstatus == 0:
    print("命令执行成功:", command_output)
else:
    print("命令执行失败:", command_output)

def login_ssh_password(user, host, password, port=22):
    # 2. 通过pexpect执行指令(有交互)
    command = 'ssh -p22 root@172.25.254.34'
    # command = 'ssh -p%s %s@%s' %(port, user, host)
    # spawn开启一个子进程处理交互式操作
    ssh = pexpect.spawn(command=command, timeout=3)
    # 匹配交互信息,返回的是匹配到的信息的索引
    match_index = ssh.expect(['Are you sure you want to continue connecting (yes/no)? ', 'password:'])
    print(match_index)
    # 如果索引为0代表第一次连接,如果索引为1代表非第一次连接
    if match_index == 0:
        print("第一次连接")
        ssh.sendline('yes')
        ssh.expect(['password:'])
        ssh.sendline(password)
    elif match_index == 1:
        print('非第一次连接')
        ssh.sendline(password)

    login_index = ssh.expect(['Last login: ', pexpect.EOF, pexpect.TIMEOUT])
    print(login_index)
    if login_index == 0:
        print("用户登录成功")
        # 进入元成服务器的命令行
        ssh.interact()
    elif login_index == 1:
        print("登录失败:Logout")
    elif login_index == 2:
        print('登录超时')

if __name__ == '__main__':
    login_ssh_password(user='root',host='172.25.254.34', password='Asimov',port=22)
pexpect模块的缺陷:
  • 依赖终端命令的方式
  • 不同的ssh登陆环境兼容性差

Python的SSH登录模块paramiko

  • 什么是paramiko?
    **paramiko是一个用于做远程控制的模块,**使用该模块可以对远程服务器进行命令或文件操作,paramiko是用python语言写的一个模块,遵循SSH2协议,支持以加密和认证的方式,进行远程服务器的连接。
  • 安装paramiko
# 使用豆瓣的镜像源, 安装paramiko模块并指定安装版本为2.6.0.
$ pip install -i https://pypi.douban.com/simple paramiko==2.6.0
  • paramiko核心组件

paramiko包含两个核心组件:SSHClient和SFTPClient(sftp=ssh file transfer protocol)。

  • SSHClient的作用类似于Linux的ssh命令,是对SSH会话的封装,该类封装了传输(Transport),通道(Channel)及SFTPClient建立的方法(open_sftp),通常用于执行远程命令。

  • SFTPClient的作用类似与Linux的sftp命令,是对SFTP客户端的封装,用以实现远程文件操作,如文件上传、下载、修改文件权限等操作。

  • 项目代码:基于paramiko实现ssh客户端密钥远程登录
    测试之前生成公钥和私钥进行测试:

# 生成公钥和私钥, 默认存储在 ~/.ssh/目录下. id_rsa私钥, id_rsa.pub公钥
ssh-keygen
# 希望我的主机可以无密码连接其他主机(需要将公钥分发给其他主机)
ssh-copy-id -i ~/.ssh/id_rsa.pub user@ip
# 测试无密码连接是否成功
ssh user@ip
ssh -i 私钥位置 user@ip
import paramiko

def login_ssh_password(hostname, port, username, password, command):
    # 实例化SSH客户端对象
    with paramiko.SSHClient() as client:
    #自动添加当前主机到远程服务器的known_hosts,远程连接不再询问yes/no
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        # 通过用户名和密码连接远程服务器
        client.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=password,
        )
        # 连接成功后执行的命令
        stdin,stdout,stderr = client.exec_command(command)
        # 获取命令执行的正确输出
        # return stdin, stdout,stderr
        return stdout.read().decode('utf-8')

def login_ssh_key(hostname, port, username, keyfile, command):
    # 实例化SSH客户端对象
    with paramiko.SSHClient() as client:
        # 配置私人密钥文件位置
        private = paramiko.RSAKey.from_private_key_file(keyfile)
        #自动添加当前主机到远程服务器的known_hosts,远程连接不再询问yes/no
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        # 通过用户名和密码连接远程服务器
        client.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=private,
        )
        # 连接成功后执行的命令
        stdin,stdout,stderr = client.exec_command(command)
        # 获取命令执行的正确输出
        # return stdin, stdout,stderr
        return stdout.read().decode('utf-8')

if __name__ == '__main__':
    # stdout = login_ssh_password(hostname='172.25.254.34',port=22,username='root',password='Asimov',command='uname')
    # print(stdout)
    stdout = login_ssh_key(hostname='172.25.254.34',port=22,username='root',keyfile='/home/kiosk/.ssh/id_rsa',command='uname')
    print(stdout)
  • SFTPClient实战代码: 上传和下载文件
    SFTPCLient作为一个sftp的客户端对象,根据ssh传输协议的sftp会话,实现远程文件操作,如上传、下载、权限、状态。
from_transport(cls,t)
客户端通道
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)

系统信息获取

  • 通过系统获取哪些信息
命令作用举例
主机名通过名称识别资产的作用、位置等信息Nginx01、KVM、aliyun01
MAC地址记录网卡的信息,可以作为主机的唯一标识6e:40:08:f9:84:00
SN物理服务器、网络设备有唯一的资产标识J156R12
系统版本查看服务器系统和具体的版本Redhat7.0、Centos7.0、Ubuntu12.04
服务器机型查看主机或者服务器类型Dell R610、HP、DL580
  • 为什么要获取这些信息

    • 有利于识别资产设备
    • 是资产的基本信息, 是自动化平台的基础(实现自动化任务执行、定时任务、自动化报表、监控等相关功能)
    • 此处只能实现主机类型信息的探测, 而不是Docker容器的探测
  • 获取信息的Linux命令介绍

    • 获取主机名的命令(选择通用方式): hostname、uname -a、cat /etc/sysconfig/network(主要针对Centos)
    • 获取系统版本: cat /etc/issue(可能为空)、cat /etc/redhat-release、uname、lsb_release
    • 获取MAC地址: cat /sys/class/net/ [^vtlsb] */address、ifconfig ens33
    • 获取服务器硬件机型: dmidecode -s system-manufacturer、dmidecode -s system-product-name

Django数据库模型设计

# apps/scanhost/models.py
class Server(models.Model):
   """服务器设备"""
   sub_asset_type_choice = (
      (0, 'PC服务器'),
      (1, '刀片机'),
      (2, '小型机'),
 
)   created_by_choice = (
      ('auto', '自动添加'),
      ('manual', '手工录入'),
 
)
   sub_asset_type = models.SmallIntegerField(choices=sub_asset_type_choice,
default=0, verbose_name="服务器类型")
   created_by = models.CharField(choices=created_by_choice, max_length=32,
default='auto', verbose_name="添加方式")
   hosted_on = models.ForeignKey('self', related_name='hosted_on_server',
                                 blank=True, null=True, verbose_name="宿主机",
on_delete=models.CASCADE)  # 虚拟机专用字段
   IP = models.CharField('IP地址', max_length=30, default='')
   MAC
= models.CharField('Mac地址', max_length=200, default='')
   model = models.CharField(max_length=128, null=True, blank=True,
verbose_name='服务器型号')
   hostname = models.CharField(max_length=128, null=True, blank=True,
verbose_name="主机名")
   os_type = models.CharField('操作系统类型', max_length=64, blank=True,
null=True)
   os_distribution = models.CharField('发行商', max_length=64, blank=True,
null=True)
   os_release = models.CharField('操作系统版本', max_length=64, blank=True,
null=True)
   def __str__(self):
       return '%s-%s' % (self.id, self.hostname)
   class Meta:
       verbose_name = '服务器'
       verbose_name_plural = "服务器"
  • models.py文件做了修改一定要生成迁移脚本并写入数据库中
python manage.py makemigrations
python manage.py migrate
# 创建超级用户用于后台登录
python manage.py createsuperuser

配置文件配置

# devops\settings\base.py
scanhosts = [
    # '127.0.0.1',
    '172.25.254.0/24',]
    # '47.92.255.98']
commands = {
    'hostname': 'hostname',
    'os_type': 'uname',
    'os_distribution': 'dmidecode  -s  system-manufacturer',
    'os_release': 'cat /etc/redhat-release',
    'MAC': 'cat /sys/class/net/`[^vtlsb]`*/address',
}

视图函数

import re
import telnetlib

import nmap
import paramiko
from django.http import HttpResponse
from django.shortcuts import render

# Create your views here.
from CMDB.settings import base
from apps.scanhost.models import Server


def get_active_hosts(hosts):
    """根据提供的网段或者IP返回存活的主机IP"""
    # 实例化对象, portScanner()类用于实现对指定主机进行端口扫描
    nm = nmap.PortScanner()
    # 以指定方式扫描指定主机或网段的指定端口
    result = nm.scan(hosts=hosts, arguments='-n ')
    return nm.all_hosts()


def is_ssh_up(host, port=22, timeout=5):
    # 实例化对象
    tn = telnetlib.Telnet(host=host, port=port)
    # read_until读取直到遇到了换行符或超时秒数。默认返回bytes类型,通过decode方法解码为字符串。
    result = tn.read_until(b'\n', timeout=timeout).decode('utf-8')
    # print(result)     #SSH-2.0-OpenSSH_7.4
    # 通过正则匹配且忽略大小写, 寻找是否ssh服务开启。
    searchObj = re.search('ssh', result, re.I)
    # 如果能匹配到内容, 说明ssh服务开启, 是Linux服务器.
    if searchObj:
        return True
    else:
        return False


def login_ssh_key(hostname, port, username, keyfile, command):
    # 实例化SSH客户端对象
    with paramiko.SSHClient() as client:
        # 配置私人密钥文件位置
        private = paramiko.RSAKey.from_private_key_file(keyfile)
        # 自动添加当前主机到远程服务器的known_hosts,远程连接不再询问yes/no
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        # 通过用户名和密码连接远程服务器
        client.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=private,
        )
        # 连接成功后执行的命令
        stdin, stdout, stderr = client.exec_command(command)
        # 获取命令执行的正确输出
        # return stdin, stdout,stderr
        return stdout.read().decode('utf-8')


def scanhost(request):
    # 访问所有要扫描的网段/IP
    for host in base.scanhosts:
        print("正在扫描%s......" % (host))
        # 获取所有可以ping通的主机IP
        active_hosts = get_active_hosts(hosts=host)
        # 一次遍历判断ssh服务是否开启
        for active_host in active_hosts:
            if is_ssh_up(active_host):
                server = Server()
                # 设置IP地址
                server.IP = active_host
                # 执行指令
                for attr, command in base.commands.items():
                    # attr ='hostname' , command = 'hostname'
                    # 存储主机名、操作系统.....指令执行的结果
                    result = login_ssh_key(active_host, 22, 'root', '/home/kiosk/.ssh/id_rsa', command)
                    setattr(server, attr, result)
                server.save()
        return HttpResponse('扫描成功')

路由配置

# devops/urls.py
urlpatterns = [
   path('admin/', admin.site.urls),
   path('scan/', scanhost)
]

后台Admin管理

# apps/scanhost/admin.py
# 可以在admin后台管理服务器信息
admin.site.register(Server)

测试

运行项目python manage.py runserver 0.0.0.0:8000

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐