Flask && Vue 虚拟机申请平台(从开发到部署)

基础设施管理平台

一、项目介绍

开发基础设施管理平台的目的是为了简化虚拟机环境申请流程,提高申请效率。

二、项目架构

2.1 架构图

![orgchart (1)](C:Usersos-zhuhgDownloadsorgchart (1).png)

​ 图1

2.2 架构说明

根据图1架构图可以看出,整个项目设计分为三部分(其实是四部分,此处已将haproxy与DNS服务器合在一起):

第一部分是客户端:客户端主要是浏览器,浏览器访问网址,展示页面。

第二部分是haproxy:haproxy的作用是隔离了2个不同的网络,作为2个网络的唯一入口。当浏览器发送请求是,会经过haproxy,如果所请求的地址没有许 可,将不法通过haproxy进入DNS服务器,请求会失败。

第三部分是后端内网服务器: 由于网站较小,Nginx和flask后端发在同一服务器上。Nginx配置反向代理的方式访问前后端资源。

三、项目目录结构介绍

.
├─infra_flask 后端主目录
│ ├─application 应用Base目录
│ │ ├─apps 子应用目录
│ │ │ ├─infra infra应用目录
│ │ │ │ ├─ init.py 初始化文件
│ │ │ │ ├─ models.py 数据库模型文件
│ │ │ │ ├─ utils 库文件目录
│ │ │ │ └─ views.py 视图文件
│ │ │ └─ init.py 初始化文件
│ │ ├─settings 配置目录
│ │ │ ├─ dev.py 开发环境配置文件
│ │ │ ├─ init.py
│ │ │ └─ prod.py 生产文件配置文件
│ │ └─utils 主库文件目录
│ │ ├─ init.py
│ │ └─ logs.py 日志配置文件
│ ├─ init.py
│ ├─ logs 日志目录
│ ├─ utils
│ ├─ manage.py 入口文件
│ └─ migrations 数据库同步目录
├─infra_web 前端主目录
│ ├─build 构建配置目录
│ ├─config 配置文件目录
│ ├─dist 打包文件存放目录
│ ├─src
│ │ ├─assets
│ │ ├─components 组件目录
│ │ │ ├─ Apply.vue 申请虚拟机组件
│ │ │ ├─ BaseInfo.vue 基础信息组件
│ │ │ ├─ CustomServer.vue 自定义服务器组件
│ │ │ ├─ ConfSearch.vue 查询路由器申请组件
│ │ │ ├─ ConsulServer.vue consul server组件
│ │ │ ├─ Docker.vue docker 组件
│ │ │ ├─ Header.vue 头部组件
│ │ │ ├─ Home.vue 主组件
│ │ │ ├─ IPDisplay.vue ip信息查询组件
│ │ │ ├─ Kafka.vue kafka组件
│ │ │ ├─ Manu.vue 导航栏组件
│ │ │ └─ Redis.vue redis组件
│ │ └─router 路由目录
│ │ └─ index.js
│ └─static
├─ip_files ip文件存放目录
└─vmware_config 虚拟机配置文件存放目录

四、 前端

4.1 前端目录结构

├─infra_web 前端主目录
│ ├─build 构建配置目录
│ ├─config 配置文件目录
│ ├─dist 打包文件存放目录
│ ├─src
│ │ ├─assets
│ │ ├─components 组件目录
│ │ │ ├─ Apply.vue 申请虚拟机组件
│ │ │ ├─ BaseInfo.vue 基础信息组件
│ │ │ ├─ CustomServer.vue 自定义服务器组件
│ │ │ ├─ ConfSearch.vue 查询路由器申请组件
│ │ │ ├─ ConsulServer.vue consul server组件
│ │ │ ├─ Docker.vue docker 组件
│ │ │ ├─ Header.vue 头部组件
│ │ │ ├─ Home.vue 主组件
│ │ │ ├─ IPDisplay.vue ip信息查询组件
│ │ │ ├─ Kafka.vue kafka组件
│ │ │ ├─ Manu.vue 导航栏组件
│ │ │ └─ Redis.vue redis组件
│ │ └─router 路由目录
│ │ └─ index.js
│ └─static

  1. 安装Node.js

    去官网:https://nodejs.org/en/download/ 下载安装包安装。安装好noedeJS然后继续安装下一步。

  2. 安装Vue

    安装完Node.js之后可以通过cmd或者pycharm的命令行工具输入以下命令安装。

    npm install --global vue-cli
    
  3. 创建项目

    创建项目可以使用命令行方式也可以使用pycharm方式创建,这里只演示命令行方式

    vue init webpack infra
    

    创建完成后切换到infra目录下,输入以下命令npm run dev 启动项目。访问http://localhost:8080,如出现以下页面说明创建成功。

    image-20210805155746944

4.3 创建主组件

在src目录下的components目录下新建一个Home.vue文件,在此项目中,Home.vue将作为主组件。

Vue文件格式如下:

image-20210805160502842

前端入口文件Home.vue

代码如下:

<template>
  <div>
    <Header></Header>
    <div class="box" >
      <div class="left_nav" >
        <Manu></Manu>
      </div>
      <div class="content">
        <br>
        <router-view />
      </div>

    </div>
  </div>


</template>

<script>
import Header from "./Header";
import Manu from "./Manu";
import axios from "axios";

export default {
  name: "Home",
  components: {
    Header,
    Manu
  },
}
</script>

<style scoped>
.demo-basic--circle {
  margin: 15px 0 0 0;
  float: left;

}
.left_nav{
   10%;
  height: 100%;
  float: left;

}
.content{
  float: right;
   86%;
  margin: 0 auto;
  height: auto;
}
.box{
   80%;
  min-height: 1000px;
  height: auto;
  margin: 0 auto;
  background-color: white;
}
</style>

此时,如果想要在浏览器中访问Home.vue中的内容,需要在App.vue中调用组件,因为Home为主组件,还需要在router目录下的index.js中添加路由。

App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
import BaseInfo from './components/BaseInfo'
import CustomServer from './components/CustomServer'
import ConsulServer from './components/ConsulServer'
import Docker from './components/Docker'
import IPDisplay from './components/IPDisplay'
import Kafka from './components/Kafka'
import Redis from './components/Redis'
import Manu from "./components/Manu";
export default {
  name: 'App',
  components: {
    BaseInfo,
    CustomServer,
    ConsulServer,
    Docker,
    Redis,
    IPDisplay,
    Kafka,
    Manu
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  /*margin-top: 60px;*/
}
body{
    margin: 0;
    border: 0;
    background-color: #f5f5f5;
}
</style>

index.js
import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

export const asyncRouterMap = [
  {
    path: "/",
    redirect: '/apply',
    component: () => import('../components/Home'),
    meta: {
      title: "基础设施管理平台",
      // permission: "home/",
    },
    children: [
      {
        path: "/apply",
        component: () => import('../components/Apply')
      },
      {
        path: "/profile",
        component: () => import('../components/ConfSearch')
      },
      {
        path: "/display",
        component: () => import('../components/IPDisplay')
      }
    ]
  },
];

/**
 * 基础路由
 * @type { *[] }
 */
export const constantRouterMap = [];
export default new Router({
  mode: "history",
  routes: constantRouterMap,
});

4.4 创建子组件

基础信息组件BaseInfo.vue
<template>
  <div class="div_style">
    <el-card class="box-card">
      <el-row>
        <el-col :span="4" class="sidebar">
          <a style="text-align: center;display: block;padding-top: 60px;font-size: 18px;font-weight: bold">基础信息</a>
        </el-col>
        <el-col :span="20">
          <el-row>
            <el-col :span="1">
              <div class="name_style">

              </div>
            </el-col>
            <el-col :span="5">
              <div class="name_style required">
                系统名
                <el-tooltip content="仅限输入英文和数字。" placement="right-start">
                   <i class="el-icon-info"></i>
                </el-tooltip>
              </div>
            </el-col>
            <el-col :span="2">
              <div class="name_style">

              </div>
            </el-col>
            <el-col :span="6">
              <div class="name_style required">
                实名登录账户
                <el-tooltip content="默认添加同小组的所有账户,如多个账户名请以英文逗号隔开" placement="right-start">
                   <i class="el-icon-info"></i>
                </el-tooltip>
              </div>
            </el-col>

            <!--          <el-col :span="8">-->
            <!--            <div class="name_style">-->
            <!--              白名单IP <span style="font-size: 12px;color: #dd6161">(使用英文逗号分隔,-->
            <!--            开发测试环境无需提供)</span>-->
            <!--            </div>-->
            <!--          </el-col>-->
            <el-col :span="2">
              <div class="name_style">

              </div>
            </el-col>
            <el-col :span="5">
              <div class="name_style required">
                网络环境
              </div>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="1">
              <div class="name_style">

              </div>
            </el-col>
            <el-col :span="5">
              <el-form :model="baseInfos" :rules="Rules">
                <el-form-item prop="systemName">
                  <el-input v-model="baseInfos.systemName" placeholder="请输入内容"></el-input>
                </el-form-item>
              </el-form>
            </el-col>
            <el-col :span="2">
              <div class="name_style">

              </div>
            </el-col>
            <el-col :span="6">
              <el-form :model="baseInfos" :rules="Rules">
                <el-form-item prop="loginName">
                  <el-input v-model="baseInfos.loginName" placeholder="请输入内容"></el-input>
                </el-form-item>
              </el-form>
            </el-col>

            <!--          <el-col :span="8">-->
            <!--            <el-input v-model="baseInfos.hostsAllow" placeholder="请输入内容"></el-input>-->
            <!--          </el-col>-->
            <el-col :span="2">
              <div class="name_style">

              </div>
            </el-col>
            <el-col :span="5">
              <el-form :model="baseInfos" :rules="Rules">
                <el-form-item prop="network">
                  <el-select v-model="baseInfos.network" clearable placeholder="请选择">
                    <el-option
                      v-for="item in options"
                      :key="item.network"
                      :label="item.label"
                      :value="item.network">
                    </el-option>
                  </el-select>
                </el-form-item>
              </el-form>
            </el-col>
          </el-row>
        </el-col>
      </el-row>
    </el-card>
  </div>
</template>

<script>
// import axios from 'axios'

export default {
  name: 'BaseInfo',
  data () {
    return {
      visible: false,
      systemName: '',
      hostsAllow: '',
      network: '',
      baseInfos: {
        systemName: this.systemName,
        loginName: this.userName,
        // hostsAllow: this.hostsAllow,
        network: this.network
      },
      options: [{
        network: 'dev:10.86.230.*',
        label: 'dev:10.86.230.*'
      }, {
        network: 'sit:10.86.132.*',
        label: 'sit:10.86.132.*'
      }, {
        network: 'puat:10.86.140.*',
        label: 'puat:10.86.140.*'
      }, {
        network: 'duat:10.86.144.*',
        label: 'duat:10.86.144.*'
      }, {
        network: 'prd:10.86.164.*',
        label: 'prd:10.86.164.*'
      }, {
        network: 'dealing:10.86.195.*',
        label: 'dealing:10.86.195.*'
      }],
      Rules: {
        systemName: [
          {required: true, message: '数据必填,不可为空!'},
          {required: true, pattern: /^[a-zA-Z0-9]+$/,message: "仅限输入英文和数字",trigger: "blur"}
        ],
        loginName: [
          {required: true, message: '数据必填,不可为空!'},
          {required: true, pattern: /^[a-zA-Z0-9-,u4e00-u9fa5]+$/,message: "请以英文逗号隔开",trigger: "blur"}
        ],
        network: [
          {required: true, message: '数据必填,不可为空!'}
        ]
      }
    }
  },
  methods: {
    handleClick (tab, event) {
      console.log(tab, event)
    }
  },
  watch: {
    baseInfos: {
      deep: true,
      handler: function () {
        this.$emit('baseInfo', this.baseInfos)
      }
    }
  },
  props: [
    'userName'
  ]

}
</script>

<style scoped>
.name_style {
  padding-top: 20px;
  font-size: 15px;
  height: 30px;
}

.required:before {
  content: "*";
  color: #f56c6c;
}

.el-input {
   85%;
  margin-top: 25px;
}

.el-select {
   85%;
  margin-top: 25px;
}

.sidebar {
  height: 150px;
  border-right: 1px solid #eceaea;
}

/deep/ .el-form-item__error {
  left: 20%;
}
</style>

样式如下:

image-20210831144416340

自定义服务器组件CustomServer.vue
<template>
  <div>
    <div>
      <el-table :data="data">
        <el-table-column prop="hostName">
          <template slot="header" >
            <span class="required"> 主机名
            <el-tooltip content="仅限输入英文和数字。" placement="right-start">
                   <i class="el-icon-info"></i>
                </el-tooltip>
            </span>
          </template>
           <template slot-scope="scope">
            <el-input v-model="data[scope.$index].hostName"></el-input>
          </template>
        </el-table-column>
        <el-table-column>
          <template  slot="header" slot-scope="scope" :prop="'data.' + scope.$index + '.appName'">
            <span>应用账户
               <el-tooltip content="如有多个账户名请以英文逗号隔开" placement="right-start">
                   <i class="el-icon-info"></i>
                </el-tooltip>
            </span>
          </template>
          <template slot-scope="scope">

            <el-input  v-model="data[scope.$index].appName"></el-input>
          </template>
<!--          <el-form :model="data" :rules="Rules">-->
<!--                <el-form-item prop="appName">-->
<!--                  <el-input v-model="data[scope.$index].appName" placeholder="请输入内容"></el-input>-->
<!--                </el-form-item>-->
<!--              </el-form>-->
        </el-table-column>
        <el-table-column prop="cpu" label="CPU">
          <template slot-scope="scope">
            <el-select v-model="data[scope.$index].CPU" placeholder="">
              <el-option
                v-for="item in cpu_count"
                :key="item.CPU"
                :label="item.cpu_label"
                :value="item.CPU">
              </el-option>
            </el-select>
          </template>
        </el-table-column>
        <el-table-column prop="mem" label="MEM">
          <template slot-scope="scope">
            <el-select v-model="data[scope.$index].MEM" placeholder="">
              <el-option
                v-for="item in mem_count"
                :key="item.MEM"
                :label="item.mem_label"
                :value="item.MEM">
              </el-option>
            </el-select>
          </template>
        </el-table-column>
        <el-table-column prop="disk_size" >
          <template slot="header" slot-scope="scope">
            <span > 额外分区
            <el-tooltip content="单位:GB,如需额外磁盘空间在此填写,此项可不填,额外磁盘分区将挂载到home目录下。" placement="right-start">
                   <i class="el-icon-info"></i>
                </el-tooltip>
            </span>
          </template>
           <template slot-scope="scope">
            <el-input v-model="data[scope.$index].disk_size"></el-input>
          </template>
        </el-table-column>
        <el-table-column prop="JDK8" label="JDK8">
          <template slot-scope="scope">
            <el-checkbox class="checkbox" v-model="data[scope.$index].JDK8"></el-checkbox>
          </template>
        </el-table-column>
        <el-table-column prop="JDK11" label="JDK11">
          <template slot-scope="scope">
            <el-checkbox class="checkbox" v-model="data[scope.$index].JDK11"></el-checkbox>
          </template>
        </el-table-column>
        <el-table-column prop="nginx" label="Nginx">
          <template slot-scope="scope">
            <el-checkbox class="checkbox" v-model="data[scope.$index].nginx"></el-checkbox>
          </template>
        </el-table-column>
        <el-table-column prop="skywalking" label="skywalking">
          <template slot-scope="scope">
            <el-checkbox class="checkbox" v-model="data[scope.$index].skywalking"></el-checkbox>
          </template>
        </el-table-column>
        <el-table-column prop="consul" label="consul">
          <template slot-scope="scope">
            <el-checkbox class="checkbox" v-model="data[scope.$index].consulclient"></el-checkbox>
          </template>
        </el-table-column>
        <el-table-column prop="ser_count" >
           <template slot="header" slot-scope="scope">
            <span class="required"> 服务器数量</span>
          </template>
          <template slot-scope="scope">
            <el-select v-model="data[scope.$index].count" placeholder="">
              <el-option
                v-for="item in sers"
                :key="item.count"
                :label="item.ser_sum"
                :value="item.count">
              </el-option>
            </el-select>
          </template>
        </el-table-column>
        <el-table-column>
          <template slot-scope="scope">
            <el-button style="margin-left: 15px" type="danger" @click="deleteRow(scope.$index)" icon="el-icon-delete"
                       circle></el-button>
          </template>
        </el-table-column>

      </el-table>
      <el-button style="margin: 30px" @click="add" icon="el-icon-circle-plus" circle></el-button>
    </div>
  </div>

</template>
<script>
// import axios from 'axios'
export default {

  data () {
    return {
      data: [{
        JDK8: false,
        JDK11: false,
        nginx: false,
        skywalking: false,
        consulclient: false,
        disk_size: ''
      }],

      count: '',
      sers: [{
        count: '1',
        ser_sum: '1'
      }, {
        count: '2',
        ser_sum: '2'
      }, {
        count: '3',
        ser_sum: '3'
      }, {
        count: '4',
        ser_sum: '4'
      }, {
        count: '5',
        ser_sum: '5'
      }, {
        count: '6',
        ser_sum: '6'
      }],
      cpu_count: [{
        CPU: '4',
        cpu_label: '4'
      }, {
        CPU: '8',
        cpu_label: '8'
      }, {
        CPU: '16',
        cpu_label: '16'
      }],
      mem_count: [{
        MEM: '4',
        mem_label: '4'
      }, {
        MEM: '8',
        mem_label: '8'
      }, {
        MEM: '16',
        mem_label: '16'
      }]
    }
  },
  methods: {
    add () {
      this.data.push({})
    },
    handleChange (value) {
      console.log(value)
    },
    deleteRow (index) {
      console.log(JSON.stringify(this.data))
      this.data.splice(index, 1)
    }
  },
  watch: {
    data () {
      this.$emit('customServer', this.data)
    }
  }
}
</script>

<style scoped>
.required:before {
  content: "*";
  color: #f56c6c;
}
</style>

样式如下:

image-20210831144508526

ConsulServer.vue
<template>
  <el-card class="box-card">
    <div slot="header">
      <span style="font-size: 18px;font-weight: bold">Consul Server集群</span>
    </div>
    <div class="text item">
      <p class="info">*说明:Consul Server集群默认配置为:4核4G, 集群名由系统自动生成。</p>
<!--      nodes:-->
      <!--      <el-select @blur="validate" @change="count" :disabled="flag=!this.consulServerInfos.install_consul" v-model="consulServerInfos.count" placeholder="请选择">-->
      <!--        <el-option-->
      <!--          v-for="item in consul_node"-->
      <!--          :key="item.count"-->
      <!--          :label="item.label"-->
      <!--          :value="item.count">-->
      <!--        </el-option>-->
      <!--      </el-select>-->
      <el-form ref="form" :model="consulServerInfos" :inline="true" :rules="consul_rules" >
        <el-form-item  :required="true" prop="count" label="安装数量">
          <el-select v-model="consulServerInfos.count" :disabled="flag=!this.consulServerInfos.isInstall" placeholder="请选择"
                     @blur="validate">
            <el-option v-for="item in consul_node" :key="item.count" :label="item.label"
                       :value="item.count"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <p></p>
      <el-checkbox class="checkbox" v-model="consulServerInfos.isInstall" label="安装应用"></el-checkbox>
      <p></p>
    </div>
  </el-card>

</template>

<script>
export default {
  name: 'AppCard',
  data() {
    return {
      flag: false,
      count: '',
      consul_node: [{
        count: '3',
        label: '3'
      }, {
        count: '4',
        label: '4'
      }, {
        count: '5',
        label: '5'
      }, {
        count: '6',
        label: '6'
      }],
      consulServerInfos: {
        count: '',
        isInstall: false
      },
      consul_rules: {
        count: [
          {required: true, message: '数据必填,不可为空!', trigger: ['blur', 'change']}
        ]
      }
    }
  },
  methods: {
    validate() {
      if (this.consulServerInfos.isInstall === true && this.consulServerInfos.count ==="" ) {
        this.$refs.form.validateField('count', (err) => {
          console.log(err)
        })
      }
    }
  },
  watch: {
    consulServerInfos: {
      deep: true,
      handler() {
        this.$emit('consulServer', this.consulServerInfos)
      }
    }
  },
}
</script>

<style scoped>
.box-card {
  margin: 10px 0;
  height: 350px;
}

.box-card:hover {
  box-shadow: #a6a9ad 1px 1px 5px 1px;
}

.info {
  color: #555555;
  font-size: 13px;
}

.instructions {
  color: rgb(153, 153, 153);
  font-size: 12px;
}
</style>

样式如下:

image-20210831144558826

ip信息展示组件IPDisplay.vue
<template>
  <div style="margin: 0 10px;">
    <br>
    <div class="tip">
      <i style="color: #dd6161;" class="el-icon-bell"></i>
      <span style="color: #dd6161;font-size: 14px">注意:</span>
      <span style="font-size: 14px">当前展示数据为 {{ this.update_time }} 的数据,使用新IP前务必与基础设施小组确认。&nbsp;&nbsp</span>
      <el-button style="display: inline"
                 size="small"
                 @click="update"
      >
        更新数据
      </el-button>
      <el-dialog :show-close=false center width="30%" title="以下网段IP信息未更新,现已发送邮件通知管理员,请注意使用。"
                 :visible.sync="dialogTableVisible">
        <table class="detail">
          <tr>
            <td v-for="(item, idx) in update_failed_files">
              {{ item }}
            </td>
          </tr>
        </table>
      </el-dialog>
    </div>
    <br>
    <el-tabs v-loading="loading" :data="ip_dict" v-model="activeName1" type="card" @tab-click="handleClick1">
      <el-tab-pane v-for="(itm,value1,idx) in ip_dict" :key="idx" :name='value1'>
        <span slot="label"> {{ value1 }}</span>.
        <el-scrollbar>
          <el-tabs class="ip" v-model="activeName2" @tab-click="handleClick2">
            <el-tab-pane label="可用IP" name="first">
              <div>
                <table class="dataintable">
                  <tr>
                    <th>IP地址</th>
                    <th>主机名</th>
                    <th>MAC地址</th>
                    <th>最后更新时间</th>
                  </tr>
                  <tr v-for="(item,val,idx) in itm.available_list" v-bind:key="idx">
                    <td>{{ item.ip }}</td>
                    <td>{{ item.hostname }}</td>
                    <td>{{ item.mac }}</td>
                    <td>{{ item.last_discovery_time }}</td>
                  </tr>
                </table>
              </div>
            </el-tab-pane>

            <el-tab-pane label="在用IP" name="second">
              <div>
                <table class="dataintable">
                  <tr>
                    <th>IP地址</th>
                    <th>主机名</th>
                    <th>MAC地址</th>
                    <th>最后更新时间</th>
                  </tr>
                  <tr v-for="(item,val,idx) in itm.using_list" v-bind:key="idx">
                    <td>{{ item.ip }}</td>
                    <td>{{ item.hostname }}</td>
                    <td>{{ item.mac }}</td>
                    <td>{{ item.last_discovery_time }}</td>
                  </tr>
                </table>
              </div>
            </el-tab-pane>
            <el-tab-pane label="预留IP" name="third">
              <div>
                <table class="dataintable">
                  <tr>
                    <th>IP地址</th>
                    <th>主机名</th>
                    <th>MAC地址</th>
                    <th>最后更新时间</th>
                    <th>备注</th>
                    <th>操作</th>
                  </tr>
                  <tr v-for="(item,val,idx) in itm.reserved_list" v-bind:key="idx">
                    <td>{{ item.ip }}</td>
                    <td>{{ item.hostname }}</td>
                    <td>{{ item.mac }}</td>
                    <td>{{ item.last_discovery_time }}</td>
                    <td contenteditable="true" @blur="edit(item.ip,get_remark($event))" v-text="item.remark">
                          {{item.remark}}
                    </td>
                    <td>
                      <el-button @click="release(item.ip)" type="text">释放</el-button>
                    </td>
                  </tr>
                </table>
                <br>
                <el-row>
                  <el-button @click="add_reserved_ip" icon="el-icon-circle-plus-outline" circle></el-button>
                </el-row>
              </div>
            </el-tab-pane>
            <el-tab-pane label="曾用IP" name="fourth">
              <div>
                <table class="dataintable">
                  <tr>
                    <th>IP地址</th>
                    <th>主机名</th>
                    <th>MAC地址</th>
                    <th>最后更新时间</th>
                    <th>备注</th>
                    <th>操作</th>
                  </tr>
                  <tr v-for="(item,val,idx) in itm.used_list" v-bind:key="idx">
                    <td>{{ item.ip }}</td>
                    <td>{{ item.hostname }}</td>
                    <td>{{ item.mac }}</td>
                    <td>{{ item.last_discovery_time }}</td>
                    <td contenteditable="true" @blur="edit(item.ip,get_remark($event))" v-text="item.remark">
                          {{item.remark}}
                    </td>
                    <td>
                      <el-button @click="release(item.ip)" type="text">释放</el-button>
                    </td>
                  </tr>
                </table>
              </div>
            </el-tab-pane>
          </el-tabs>
        </el-scrollbar>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script>
import axios from "axios";

export default {
  name: 'IPDisplay',
  data() {
    return {
      remark: '',
      ip:'',
      ip_dict: {},
      loading: false,
      update_failed_files: [],
      activeName1: 'db',
      activeName2: 'first',
      dialogTableVisible: false,
      update_time: '',
      dialogVisible: false,
    }
  },
  methods: {
    get_remark($event){
      this.remark = $event.target.innerText;
      return this.remark
    },
    edit(ip,remark){
      console.log(ip,remark)
      axios
          .post("http://127.0.0.1:5000/display/edit", {
            ip: ip,
            remark: remark
          })
          .then((response) => {
            console.log(response.data)
            this.ip_dict = response.data.ip_dict;
            this.update_time = response.data.update_time
          })
          .catch(function (error) {
            console.log(error)
          });
    },
    handleClose(done) {
      this.$confirm('确认关闭?')
        .then(_ => {
          done();
        })
        .catch(_ => {
        });
    },
    handleClick1(tab, event) {
      console.log(tab, event)
    },
    handleClick2(tab, event) {
      console.log(tab, event)
    },
    handleSelect(key, keyPath) {
      console.log(key, keyPath)
    },
    update() {
      this.loading = true
      axios
        .get("http://127.0.0.1:5000/display/update",)
        .then((response) => {
          console.log(response)
          if (response.data === 'ip信息无需频繁更新,请稍后再试!') {
            this.$alert(response.data, '提示', {
              confirmButtonText: '确定',
            });
            this.loading = false

          } else {
            this.ip_dict = response.data.ip_dict;
            this.loading = false;
            this.update_time = response.data.update_time
            this.update_failed_files = response.data.update_failed_files
            if (response.data.update_failed_files.length > 0) {
              this.dialogTableVisible = true
            }
          }
        })
        .catch(function (error) {
          console.log(error);
        });
    },
    search_ip() {
      axios
        .get("http://127.0.0.1:5000/display",)
        .then((response) => {
          console.log(response.data)
          this.ip_dict = response.data.ip_dict;
          this.update_time = response.data.update_time
          this.loading = false;
        })
        .catch(function (error) {
          console.log(error);
        });
    },
    release(ip) {
      this.$confirm('此操作将会把当前IP变为可用IP,为防止IP占用,在此操作前请务必确认此IP是否符合释放条件, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        axios
          .post("http://127.0.0.1:5000/display/release", {
            ip: ip
          })
          .then((response) => {
            console.log(response.data)
            this.ip_dict = response.data.ip_dict;
            this.update_time = response.data.update_time
            this.loading = false;
            this.$message({
              type: 'success',
              message: '当前IP:' + ip + '释放成功!'
            });
          })
          .catch(function (error) {
            console.log(error)
          });
      }).catch((error) => {
        this.$message({
          type: 'error',
          message: error
        });
      })

    },
    add_reserved_ip() {
      this.$prompt('请输入预留IP', '新增预留IP', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        inputPattern: /^(?:(?:^|,)(?:[0-9]|[1-9]d|1d{2}|2[0-4]d|25[0-5])(?:.(?:[0-9]|[1-9]d|1d{2}|2[0-4]d|25[0-5])){3})+$/,
        inputErrorMessage: 'IP地址格式不正确。如多个ip,以英文逗号隔开'
      }).then(({value}) => {
        axios
          .post("http://127.0.0.1:5000/display/addReserved", {
            ip: value
          })
          .then((response) => {
            console.log(response.data)
            this.ip_dict = response.data.ip_dict;
            this.update_time = response.data.update_time
            this.loading = false;
            this.$message({
              type: 'success',
              message: '预留IP: ' + value + '添加成功'
            });
          })
          .catch(function (error) {

          });

      }).catch((error) => {
        this.$message({
          type: 'error',
          message: error
        });
      })
    }
  },
  created() {
    this.search_ip()
  },
  // props: [
  //   'ip_list'
  // ]
}
</script>

<style scoped>

.tip {
  text-align: left;
}

.el-scrollbar__thumb {
  display: none;
}

.el-scrollbar__wrap {
  overflow-x: hidden;
  overflow-y: auto;
}

/deep/ .el-loading-spinner {
  position: unset;
}

.ip {
   85%;
  height: 800px;
  overflow: -moz-scrollbars-none;
  margin-left: 5%;

}


table.dataintable {
  margin-top: 15px;

  border-collapse: collapse;
   100%;
  border-bottom: 1px solid #ebeef5;
  box-sizing: border-box;
  text-overflow: ellipsis;
  white-space: normal;
  word-break: break-all;
  line-height: 23px;
  padding-left: 10px;
  padding-right: 10px;
}

table.dataintable th {
  vertical-align: baseline;
  text-align: center;
  padding: 5px 15px 5px 6px;
  color: #606266;
  border-bottom: 1px solid #ebeef5;
}

table.dataintable td {
  vertical-align: text-top;
  padding: 6px 15px 6px 6px;
  /*border-bottom: 1px solid #aaa;*/
  border-bottom: 1px solid #ebeef5;
}

tr:hover {
  background-color: #F5F5F5;
}

</style>

样式如下:

image-20210831144709878

Apply.vue
<template>
  <div>
    <!--      基础信息-->
    <div class="div_style">
      <BaseInfo :userName="userName" @baseInfo="baseInfo"></BaseInfo>
    </div>

    <!--      自定义组合服务器-->
    <div class="div_style">
      <el-card class="box-card">
        <div slot="header" class="clearfix">
                <span
                  style="
                    text-align: center;
                    display: block;
                    font-size: 18px;
                    font-weight: bold;
                  "
                >自定义组合服务器</span
                >
        </div>
        <CustomServer @customServer="CustomServerInfo"></CustomServer>
      </el-card>
    </div>

    <!--      应用配置展示卡-->
    <div style="margin-top: 10px">
      <el-row>
        <el-col :span="6">
          <div class="grid-content bg-purple">
            <ConsulServer
              @consulServer="consulServerInfos"
            ></ConsulServer>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="grid-content bg-purple-light">
            <Kafka @kafka="kafkaInfos"></Kafka>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="grid-content bg-purple">
            <Redis @redis="redisInfos"></Redis>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="grid-content bg-purple">
            <Docker @docker="dockerInfos"></Docker>
          </div>
        </el-col>
      </el-row>
    </div>
    <br>
    <!--    提交按钮-->
    <el-row class="btn-style">
      <el-col :span="24" style="padding-right: 10px">
        <el-button
          type="primary"
          :disabled="btnStatus"
          @click="submit"
          plain
        >开始构建
        </el-button>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import BaseInfo from "./BaseInfo";
import Docker from "./Docker";
import axios from "axios";
import ConsulServer from "./ConsulServer";
import Kafka from "./Kafka";
import Redis from "./Redis";
import CustomServer from "./CustomServer";
import IPDisplay from "./IPDisplay";
import Manu from "./Manu";
import {getUserName, logout} from "igw-library/lib/sdk";

export default {
  name: "Apply",
  components: {
    ConsulServer,
    Kafka,
    Redis,
    CustomServer,
    Docker,
    BaseInfo,
    IPDisplay
  },
  data() {
    return {
      userName: getUserName(),
      appName: "",
      btnStatus: false,
      config_list: [],
      ip_dict: {},
      table_data: [{}],
      activeName: "1",
      table: false,
      dialog: false,
      loading: false,
      drawer: false,
      dialogTableVisible: false,
      delete_flag: {},
      num: "",
      activeTab: "first",
      circleUrl: "../assets/user.png",
      username: "",
      roles: [],
      superUser: []
    };
  },
  methods:{
    CustomServerInfo(data) {
      this.customServerinfos = data;
    },
    baseInfo(data) {
      this.BaseInfo = data;
    },
    consulServerInfos(data) {
      this.ConsulServerInfos = data;
    },
    kafkaInfos(data) {
      this.KafkaInfos = data;
    },
    redisInfos(data) {
      this.RedisInfos = data;
    },
    dockerInfos(data) {
      this.DockerInfos = data;
    },
    handleClose(key, keyPath) {
      console.log(key, keyPath);
    },
    handleClick(tab, event) {
      console.log(tab, event);
    },
    err_box() {
      this.$notify.error({
        title: "请检查带“红色*”号的选项是否填写!",
        message: this.$createElement(
          "i",
          {style: "color: red"},
          "请在必填信息填写完成后继续!"
        ),
      });
    },
    submit() {
      let flag = 0;
      try {
        if (
          this.ConsulServerInfos["isInstall"] &&
          this.ConsulServerInfos["count"] === ""
        ) {
          return this.err_box();
        }
      } catch (err) {
      }
      try {
        if (this.KafkaInfos["isInstall"] && this.KafkaInfos["count"] === "") {
          return this.err_box();
        }
      } catch (err) {
      }
      try {
        if (this.RedisInfos["isInstall"] && this.RedisInfos["count"] === "") {
          return this.err_box();
        }
      } catch (err) {
      }
      try {
        if (
          this.DockerInfos["isInstall"] === true &&
          this.DockerInfos["count"] === ""
        ) {
          this.err_box();
          return this.err_box();
        }
      } catch (err) {
      }

      if (
        Boolean(this.customServerinfos) === true &&
        Boolean(this.BaseInfo) === true
      ) {
        if (
          Boolean(this.BaseInfo["systemName"]) === true &&
          Boolean(this.BaseInfo["loginName"]) === true &&
          Boolean(this.BaseInfo["network"]) === true
        ) {
          for (var i = 0; i < this.customServerinfos.length; i++) {
            if (
              "hostName" in this.customServerinfos[i] &&
              "count" in this.customServerinfos[i]
            ) {
              flag = flag + 1;
            }
          }
        }
        if (flag === this.customServerinfos.length) {
          axios
            .post("http://127.0.0.1:5000/apply", {
              BaseInfos: this.BaseInfo,
              consulServerInfos: this.ConsulServerInfos,
              kafkaInfos: this.KafkaInfos,
              redisInfos: this.RedisInfos,
              dockerInfos: this.DockerInfos,
              groupInfos: this.customServerinfos,
            })
            .then((response) => {
              this.config_list = response.data["info"];
              // this.config_list.forEach((i) => {
              //   this.$set(i, "dialogTableVisible", false);
              // });
              this.btnStatus = true;
              this.$notify({
                title: "配置信息提交成功!",
                message: this.$createElement(
                  "i",
                  {style: "color: teal"},
                  "虚拟机将在您完成OA流程后开始创建!创建完成后,您将收到管理员的通知邮件。"
                ),
              });
            })
            .catch((error) => {
              // 请求失败处理
              console.log(error);
              this.$notify.error({
                title: "错误",
                message:
                  "此配置已存在,请在现有申请中确认您当前填写的数据是否已存在!",
              });
            });
        } else {
          this.err_box();
        }
      } else {
        this.err_box();
      }
    },


  }
}
</script>

<style scoped>
.user {
   100px;
}

.demo-basic--circle {
  margin: 15px 0 0 0;
  float: left;
}

.el-input {
   85%;
  margin-top: 25px;
}

.el-select {
   85%;
  margin-top: 25px;
}

.div_style {
  margin-top: 10px;
  /*border-bottom: 1px solid #f5f5fa;*/
  background-color: #ffffff;
}

.div_style:hover {
  box-shadow: #a6a9ad 1px 1px 5px 1px;
}

table.dataintable {
  margin-top: 15px;

  border-collapse: collapse;
   100%;
  border-bottom: 1px solid #ebeef5;
  box-sizing: border-box;
  text-overflow: ellipsis;
  white-space: normal;
  word-break: break-all;
  line-height: 23px;
  padding-left: 10px;
  padding-right: 10px;
}

table.dataintable th {
  vertical-align: baseline;
  text-align: center;
  padding: 5px 15px 5px 6px;
  /*border: 1px solid #3F3F3F;*/
  color: #606266;
  border-bottom: 1px solid #ebeef5;
}

table.dataintable td {
  vertical-align: text-top;
  padding: 6px 15px 6px 6px;
  border-bottom: 1px solid #aaa;
  border-bottom: 1px solid #ebeef5;
}

tr:hover {
  background-color: #f5f5f5;
}

.detail {
  padding: 12px 0;
  min- 0;
  box-sizing: border-box;
  text-overflow: ellipsis;
  vertical-align: middle;
  position: relative;
  text-align: center;
  border-collapse: collapse;
   100%;
  border-bottom: 1px solid #ebeef5;
  white-space: normal;
  word-break: break-all;
  line-height: 23px;
}

.detail td {
  display: table-cell;
}
</style>

页面展示:

image-20210831144800708

Manu.vue
<template>
  <div id="val">
    <el-menu
      default-active="1"
      class="el-menu-vertical-demo">
      <router-link :to="'/apply'">
        <el-menu-item index="1">
          <i class="el-icon-plus"></i>
          <span style="font-size: 16px" slot="title">申请虚拟机</span>
        </el-menu-item>
      </router-link>
      <router-link :to="'/profile'">
        <el-menu-item index="2">
          <i class="el-icon-search"></i>
          <span style="font-size: 16px" slot="title">&nbsp;现&nbsp;有&nbsp;申&nbsp;请</span>
        </el-menu-item>
      </router-link>
      <router-link :to="'/display'">
        <el-menu-item index="3">
          <i class="el-icon-search"></i>
          <span style="font-size: 16px" slot="title">&nbsp;IP&nbsp;&nbsp;&nbsp;&nbsp;查&nbsp;&nbsp;询</span>
        </el-menu-item>
      </router-link>

    </el-menu>


  </div>
</template>

<script>


export default {
  name: "navigation",
  data() {
    return {
      isCollapse: true,

    };
  },
  methods: {
    handleOpen(key, keyPath) {
      console.log(key, keyPath);
    },
    handleClose(key, keyPath) {
      console.log(key, keyPath);
    },
  },
};
</script>

<style scoped>
/deep/ .el-menu-item {
  height: 50px;
  line-height: 50px;
  padding: 0 45px;
  min- 190px;
}
.el-menu-item.is-active {
  /*background-color:#242f42*/
}
.el-menu-vertical-demo{
  height: auto;
  /*min-height: 200px;*/
   133%;
  padding-bottom: 130%;
}
</style>

页面展示:

image-20210831145007364

ConfSearch.vue
<template>
  <div>
    <table class="dataintable">
      <tr>
        <th>系统名</th>
        <th>实名登录账户</th>
        <th>网络环境</th>
        <th>申请时间</th>
        <th></th>
      </tr>
      <tr v-for="(config, index) in config_list" v-bind:key="index">
        <td>{{ config_list[index]["BaseInfos"]["systemName"] }}</td>
        <td>{{ config_list[index]["BaseInfos"]["loginName"] }}</td>
        <td>{{ config_list[index]["BaseInfos"]["network"] }}</td>
        <td>{{ config_list[index]["BaseInfos"]["CreateDate"] }}</td>
        <td>
          <el-button
            @click="config.dialogTableVisible = true"
            type="text"
            style="margin-left: 16px"
          >详情&nbsp;&nbsp;&nbsp;
          </el-button>
          <el-button
            icon="el-icon-delete"
            class="delete"
            @click="delete_file(index)"
            type="text"
          ></el-button>
        </td>
        <el-dialog
          title="配置详情"
          :visible.sync="config.dialogTableVisible"
        >
          <table class="detail">
            <tr>
              <th>Consul Server节点数</th>
              <th>Kafka/Zookeeper节点数</th>
              <th>Redis数量</th>
              <th>Docker数量</th>
            </tr>
            <tr>
              <td>
                {{ config_list[index]["consulServerInfos"]["count"] }}
              </td>
              <td>{{ config_list[index]["kafkaInfos"]["count"] }}</td>
              <td>{{ config_list[index]["redisInfos"]["count"] }}</td>
              <td>{{ config_list[index]["dockerInfos"]["count"] }}</td>
            </tr>
          </table>
          <br/>
          <br/>
          <table class="detail">
            <tr>
              <th>主机名</th>
              <th>应用账户</th>
              <th>CPU</th>
              <th>MEM</th>
              <th>额外存储</th>
              <th>JDK8</th>
              <th>JDK11</th>
              <th>Nginx</th>
              <th>skywalking</th>
              <th>counsulclient</th>
              <th>服务器数量</th>
            </tr>
            <tr
              v-for="(item, idx) in config_list[index]['groupInfos']"
              :key="idx"
            >
              <td>{{ item.hostName }}</td>
              <td>{{ item.appName }}</td>
              <td>{{ item.CPU }}</td>
              <td>{{ item.MEM }}</td>
              <td>{{ item.disk_size }}</td>
              <td>{{ item.JDK8 }}</td>
              <td>{{ item.JDK11 }}</td>
              <td>{{ item.nginx }}</td>
              <td>{{ item.skywalking }}</td>
              <td>{{ item.consulclient }}</td>
              <td>{{ item.count }}</td>
            </tr>
          </table>
        </el-dialog>
      </tr>
    </table>
  </div>
</template>

<script>
import axios from "axios";
import {getUserName} from "igw-library/lib/sdk";

export default {
  name: "ConfSearch",
  data() {
    return {
      userName: getUserName(),
      appName: "",
      btnStatus: false,
      config_list: [],
      ip_dict: {},
      table_data: [{}],
      activeName: "1",
      table: false,
      dialog: false,
      loading: false,
      drawer: false,
      dialogTableVisible: false,
      delete_flag: {},
      num: "",
      activeTab: "first",
      circleUrl: "../assets/user.png",
      username: "",
      roles: [],
      superUser: []
    };
  },
  methods: {
    open(msg) {
      this.$confirm(msg, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          axios
            .post("http://127.0.0.1:5000/profile/delete", {
              delete_flag: this.delete_flag,
              flag: "delete",
            })
            .then((response) => {
              let msg = response.data["message"];
              this.conf_search();
              this.$message({
                type: "success",
                message: msg,
              });
            })
            .catch(function (error) {
              let msg = error.data["message"];
              open(msg, "error");
              console.log(error);
            });

        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
    in_array(searchString, array) {
      for (let i = 0; i < array.length; i++) {
        if (searchString === array[i]) return true;
      }
      return false;
    },
    delete_file(index) {
      let self = index;
      this.delete_flag = this.config_list[self]["BaseInfos"];
      let curruntUser = this.config_list[self]["BaseInfos"]["loginName"];
      console.log(this.in_array(this.userName, this.superUser))
      if (this.in_array(this.userName, this.superUser)) {
        let msg =
          "此操作仅限删除配置文件,如需删除虚拟机,请联系管理员。配置文件在删除后不可恢复,是否确认删除?";
        this.open(msg);
      } else {
        if (curruntUser === this.userName) {
          let msg =
            "此操作仅限删除配置文件,如需删除虚拟机,请联系管理员。配置文件在删除后不可恢复,是否确认删除?";
          this.open(msg);
        } else {
          let msg = "您当前没有删除其他用户申请文件的权限。";
          this.$alert(msg, "提示", {
            confirmButtonText: "确定",
            type: "warning",
            center: true,
            showClose: false,
          });
        }
      }
    },
    conf_search() {
      axios
        .get("http://127.0.0.1:5000/profile")
        .then((response) => {
          this.config_list = response.data["info"];
          this.superUser = response.data["superUser"]
          this.config_list.forEach((i) => {
            this.$set(i, "dialogTableVisible", false);
          });
        })
        .catch(function (error) {
          console.log(error);
        });
    }
  },
  created() {
    this.conf_search()
  }
}
</script>

<style scoped>
table.dataintable {
  margin-top: 15px;

  border-collapse: collapse;
   100%;
  border-bottom: 1px solid #ebeef5;
  box-sizing: border-box;
  text-overflow: ellipsis;
  white-space: normal;
  word-break: break-all;
  line-height: 23px;
  padding-left: 10px;
  padding-right: 10px;
}

table.dataintable th {
  vertical-align: baseline;
  text-align: center;
  padding: 5px 15px 5px 6px;
  /*border: 1px solid #3F3F3F;*/
  color: #606266;
  border-bottom: 1px solid #ebeef5;
}

table.dataintable td {
  vertical-align: text-top;
  padding: 6px 15px 6px 6px;
  border-bottom: 1px solid #aaa;
  border-bottom: 1px solid #ebeef5;
}

tr:hover {
  background-color: #f5f5f5;
}

.detail {
  padding: 12px 0;
  min- 0;
  box-sizing: border-box;
  text-overflow: ellipsis;
  vertical-align: middle;
  position: relative;
  text-align: center;
  border-collapse: collapse;
   100%;
  border-bottom: 1px solid #ebeef5;
  white-space: normal;
  word-break: break-all;
  line-height: 23px;
}

.detail td {
  display: table-cell;
}
</style>

页面展示:

image-20210831145122895

Docker.vue
<template>
  <el-card class="box-card">
    <div slot="header" class="clearfix">
      <span style="font-size: 18px;font-weight: bold">Docker安装</span>
    </div>
    <div class="text item">
      <p class="info">*说明:Docker默认安装版本为20.10.7。</p>
      <br>
      <el-form style="display: inline"  ref="form" :model="dockerInfos" :inline="true" :rules="rules" >
        <el-form-item  :required="true" prop="count" label="安装数量">
          <el-select v-model="dockerInfos.count" :disabled="flag=!this.dockerInfos.isInstall" placeholder="请选择"
                     @blur="validate">
            <el-option v-for="item in docker_num" :key="item.count" :label="item.label"
                       :value="item.count"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <p></p>
      <el-checkbox class="checkbox" v-model="dockerInfos.isInstall" label="安装应用"></el-checkbox>
      <p></p>
    </div>
  </el-card>

</template>

<script>
export default {
  name: 'AppCard',
  data () {
    return {
      flag: false,
      docker_num: [{
        count: '2',
        label: '2'
      }, {
        count:'3',
        label: '3'
      }, {
        count: '4',
        label: '4'
      }, {
        count: '5',
        label: '5'
      }, {
        count: '6',
        label: '6'
      }],
      dockerInfos: {
        count: '',
        isInstall: false
      },
      rules: {
        count: [
          {required: true, message: '数据必填,不可为空!', trigger: ['blur', 'change']}
        ]
      }
    }
  },
  methods: {
    validate() {
      if (this.dockerInfos.isInstall && this.dockerInfos.count ==="" ) {
        this.$refs.form.validateField('count', (err) => {
          console.log(err)
        })
      }
    }
  },
  watch: {
    dockerInfos: {
      deep: true,
      handler () {
        this.$emit('docker', this.dockerInfos)
      }
    }
  }
}
</script>

<style scoped>
.box-card{
  margin: 10px 0 0  15px;
  height: 350px;
}
.box-card:hover{
  box-shadow: #a6a9ad 1px 1px 5px 1px;
}
.info{
  color: #555555;
  font-size: 13px;
}
.instructions{
  color: rgb(153, 153, 153);
  font-size: 12px;
}
</style>

页面展示:

image-20210831145206509

Header.vue
<template>
  <div>
    <!--    header-->
    <el-header
      height="70px"
      style="background-color: #242f42;  80%; margin: 0 10%"
    >
      <el-row :gutter="20">
        <el-col :span="4">
          <div class="grid-content bg-purple">
            <img style="padding-top: 8px" src="../assets/logo.png"/>
          </div>
        </el-col>
        <el-col :span="16">
          <div
            class="grid-content bg-purple"
            style="padding: 15px 0 0 0; font-size: 30px; color: azure"
          >
            &nbsp;基&nbsp;&nbsp;础&nbsp;&nbsp;设&nbsp;&nbsp;施&nbsp;&nbsp;管&nbsp;&nbsp;理&nbsp;&nbsp;平&nbsp;&nbsp;台
          </div>
        </el-col>
        <el-col :span="1">
          <div class="grid-content bg-purple"><p></p></div>
        </el-col>
        <el-col :span="3" class="demo-basic--circle">
          <div class="block" style="float: left">
            <i-avatar :size="40"><i :size="60" class="el-icon-user-solid"/></i-avatar>
          </div>
          <span type="text" style="color: #ffffff; padding-top: 35px">{{
              this.userName
            }}</span>
          <el-button type="text" @click="logout">注销</el-button>
        </el-col>
      </el-row>
    </el-header>
  </div>
</template>

<script>
import {getUserName, logout} from "igw-library/lib/sdk";

export default {
  name: "Header",
  data() {
    return {
      userName: getUserName(),
    }
  },
  methods: {
    logout() {
      logout();
    },
  }
}
</script>

<style scoped>
.demo-basic--circle {
  margin: 14px 0 0 0;
  float: left;
}
</style>

页面展示:

image-20210831145251147

Kafka.vue
<template>
  <el-card class="box-card">
    <div slot="header" class="clearfix">
      <span style="font-size: 18px;font-weight: bold">Kafka/Zookeeper集群</span>
    </div>
    <div class="text item">
      <p class="info">*说明:Kafka/Zookeeper集群默认配置为:4核4G, 数据目录默认为:/data 。</p>

<!--      数量:-->
<!--      <el-select id="edit" :disabled="flag=!this.kafkaInfos.install_kafka" v-model="kafkaInfos.count" placeholder="请选择">-->
<!--        <el-option-->
<!--          v-for="item in kafka_num"-->
<!--          :key="item.count"-->
<!--          :label="item.label"-->
<!--          :value="item.count">-->
<!--        </el-option>-->
<!--      </el-select>-->
      <el-form style="display: inline"  ref="form" :model="kafkaInfos" :inline="true" :rules="rules" >
        <el-form-item  :required="true" prop="count" label="安装数量">
          <el-select v-model="kafkaInfos.count" :disabled="flag=!this.kafkaInfos.isInstall" placeholder="请选择"
                     @blur="validate">
            <el-option v-for="item in kafka_num" :key="item.count" :label="item.label"
                       :value="item.count"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <p></p>
      <el-checkbox class="checkbox" v-model="kafkaInfos.isInstall" label="安装应用"></el-checkbox>
      <p></p>
    </div>
  </el-card>

</template>

<script>
export default {
  name: 'AppCard',
  data () {
    return {
      flag: false,
      radio: '',
      kafka_num: [{
        count: '3',
        label: '3'
      }, {
        count: '4',
        label: '4'
      }, {
        count: '5',
        label: '5'
      }, {
        count: '6',
        label: '6'
      }],
      kafkaInfos: {
        count: '',
        isInstall: false
      },
      rules: {
        count: [
          {required: true, message: '数据必选,不可为空!', trigger: ['blur', 'change']}
        ]
      }
    }
  },
  methods: {
    validate() {
      if (this.kafkaInfos.isInstall && this.kafkaInfos.count ==="" ) {
        this.$refs.form.validateField('count', (err) => {
          console.log(err)
        })
      }
    }
  },
  watch: {
    kafkaInfos: {
      deep: true,
      handler () {
        this.$emit('kafka', this.kafkaInfos)
      }
    }
  }
}
</script>

<style scoped>
.box-card {
  margin: 10px 0 0 15px;
  height: 350px;
}

.box-card:hover {
  box-shadow: #a6a9ad 1px 1px 5px 1px;
}

.info {
  color: #555555;
  font-size: 13px;
}

.instructions {
  color: rgb(153, 153, 153);
  font-size: 12px;
}
</style>

页面展示:

image-20210831145325060

Redis.vue
<template>
  <el-card class="box-card">
    <div slot="header" class="clearfix">
      <span style="font-size: 18px;font-weight: bold">Kafka/Zookeeper集群</span>
    </div>
    <div class="text item">
      <p class="info">*说明:Kafka/Zookeeper集群默认配置为:4核4G, 数据目录默认为:/data 。</p>

<!--      数量:-->
<!--      <el-select id="edit" :disabled="flag=!this.kafkaInfos.install_kafka" v-model="kafkaInfos.count" placeholder="请选择">-->
<!--        <el-option-->
<!--          v-for="item in kafka_num"-->
<!--          :key="item.count"-->
<!--          :label="item.label"-->
<!--          :value="item.count">-->
<!--        </el-option>-->
<!--      </el-select>-->
      <el-form style="display: inline"  ref="form" :model="kafkaInfos" :inline="true" :rules="rules" >
        <el-form-item  :required="true" prop="count" label="安装数量">
          <el-select v-model="kafkaInfos.count" :disabled="flag=!this.kafkaInfos.isInstall" placeholder="请选择"
                     @blur="validate">
            <el-option v-for="item in kafka_num" :key="item.count" :label="item.label"
                       :value="item.count"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <p></p>
      <el-checkbox class="checkbox" v-model="kafkaInfos.isInstall" label="安装应用"></el-checkbox>
      <p></p>
    </div>
  </el-card>

</template>

<script>
export default {
  name: 'AppCard',
  data () {
    return {
      flag: false,
      radio: '',
      kafka_num: [{
        count: '3',
        label: '3'
      }, {
        count: '4',
        label: '4'
      }, {
        count: '5',
        label: '5'
      }, {
        count: '6',
        label: '6'
      }],
      kafkaInfos: {
        count: '',
        isInstall: false
      },
      rules: {
        count: [
          {required: true, message: '数据必选,不可为空!', trigger: ['blur', 'change']}
        ]
      }
    }
  },
  methods: {
    validate() {
      if (this.kafkaInfos.isInstall && this.kafkaInfos.count ==="" ) {
        this.$refs.form.validateField('count', (err) => {
          console.log(err)
        })
      }
    }
  },
  watch: {
    kafkaInfos: {
      deep: true,
      handler () {
        this.$emit('kafka', this.kafkaInfos)
      }
    }
  }
}
</script>

<style scoped>
.box-card {
  margin: 10px 0 0 15px;
  height: 350px;
}

.box-card:hover {
  box-shadow: #a6a9ad 1px 1px 5px 1px;
}

.info {
  color: #555555;
  font-size: 13px;
}

.instructions {
  color: rgb(153, 153, 153);
  font-size: 12px;
}
</style>

页面展示:

image-20210831145347660

五、 后端

5.1 安装flask

可以通过cmd或者pycharm的命令行工具输入以下命令安装。

pip3 install Flask

5.2 创建flask项目

5.2.1 项目目录

├─infra_flask 后端主目录
│ ├─application 应用Base目录
│ │ ├─apps 子应用目录
│ │ │ ├─infra infra应用目录
│ │ │ │ ├─ init.py 初始化文件
│ │ │ │ ├─ models.py 数据库模型文件
│ │ │ │ ├─ utils 库文件目录
│ │ │ │ └─ views.py 视图文件
│ │ │ └─ init.py 初始化文件
│ │ ├─settings 配置目录
│ │ │ ├─ dev.py 开发环境配置文件
│ │ │ ├─ init.py
│ │ │ └─ prod.py 生产文件配置文件
│ │ └─utils 主库文件目录
│ │ ├─ init.py
│ │ └─ logs.py 日志配置文件
│ ├─ init.py
│ ├─ logs 日志目录
│ ├─ utils
│ ├─ manage.py 入口文件
│ └─ migrations 数据库同步目录

5.2.2创建项目

image-20210805175321526

5.2.3 创建目录结构

目录结构如下图:

image-20210831145738429

5.2.4 flask启动文件manage.py

from application import app

# 注册模型
from infra.models import *


if __name__ == '__main__':
    app.manage.run()

5.2.5 项目初始化(application)

init.py

from flask import Flask
from application.settings.dev import DevConfig
from application.settings.prod import ProdConfig
# from flask_wtf import CSRFProtect
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_migrate import *
from application.utils.logs import setup_log
from flask_cors import CORS

# 加载配置
config_dict = {
    "dev": DevConfig,
    "prod": ProdConfig
}

db = SQLAlchemy()


def init_app(config_name):
    """项目初始化函数"""
    app = Flask(__name__)

    # 根据选项获取配置类
    Config = config_dict.get('dev')

    # 加载配置
    app.config.from_object(Config)

    # 日志初始化
    setup_log(Config)
    app.log = logging.getLogger()

    # 初始化数据库
    db.init_app(app)

    # 开启CSRF防范功能
    # CSRFProtect(app)
    # 跨域
    CORS(app, supports_credentials=True)

    # 初始化终端脚本
    app.manage = Manager(app)

    # 初始化数据迁移
    Migrate(app, db)
    app.manage.add_command("db", MigrateCommand)

    # 配置视图的存储目录为默认导包路径
    sys.path.insert(0, Config.BASE_DIR)
    res = sys.path.insert(0, os.path.join(Config.BASE_DIR, "apps"))
    sys.path.insert(0, os.path.join(Config.BASE_DIR, "utils"))

    # 蓝图注册
    from infra import infra_blu
    app.register_blueprint(infra_blu, prefix="")

    return app


app = init_app('dev')

5.2.6 项目设置(application/settings)

init.py

import os


class Config(object):
    """配置类"""

    """密钥"""
    # 密钥,可以通过 base64.b64encode(os.urandom(48)) 来生成一个指定长度的随机字符串
    SECRET_KEY = "ghhBljAa0uzw2afLqJOXrukORE4BlkTY/1vaMuDh6opQ3uwGYtsDUyxcH62Aw3ju"
    """当前项目主应用目录"""
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    """调试模式"""
    DEBUG = True

    """日志管理"""
    LOG_FILE_PATH = "logs/infra_flask.log"
    LOG_FILE_SIZE = 1024 * 1024 * 300
    LOG_FILE_NUMBER = 10
    LOG_LEVEL = "DEBUG"

    """数据库"""
    # mysql数据库的配置信息
    SQLALCHEMY_DATABASE_URI = ""
    # 动态追踪修改设置,如未设置只会提示警告
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    # 查询时会显示原始SQL语句
    SQLALCHEMY_ECHO = False

dev.py

from . import Config


class DevConfig(Config):
    """开发模式下的配置"""
    # 查询时会显示原始SQL语句
    SQLALCHEMY_ECHO = False
    SQLALCHEMY_DATABASE_URI = "mysql://root:123456@10.86.130.145:3306/infra?charset=utf8"

prod.py

from . import Config


class ProdConfig(Config):
    """生产模式下的配置"""
    DEBUG = False
    SQLALCHEMY_ECHO = False

5.2.7 配置日志功能(application/utils/logs.py)

import logging
from logging.handlers import RotatingFileHandler


def setup_log(Config):
    # 设置日志的记录等级
    logging.basicConfig(level=Config.LOG_LEVEL)
    # 创建日志记录器,指明日志保存的路径、每个日志文件的最大大小、保存的日志文件个数上限
    file_log_handler = RotatingFileHandler(
        Config.LOG_FILE_PATH,
        maxBytes=Config.LOG_FILE_SIZE, backupCount=Config.LOG_FILE_NUMBER)
    # 创建日志记录的格式 日志等级 输入日志信息的文件名 行数 日志信息
    formatter = logging.Formatter('%(levelname)s %(asctime)s %(filename)s:%(lineno)d %(message)s')
    # 为刚创建的日志记录器设置日志记录格式
    file_log_handler.setFormatter(formatter)
    # 为全局的日志工具对象(flaskapp使用的)添加日志记录器
    logging.getLogger().addHandler(file_log_handler)

5.2.8 子应用(application/apps)

5.2.8.1 第一个子应用(infra)
初始化文件__init__.py,初始化蓝图
from flask import Blueprint

infra_blu = Blueprint("infra", __name__)

from .views import *
配置数据库模型文件models.py
import datetime

from application import db
from sqlalchemy import ForeignKey, DateTime
from sqlalchemy.orm import relationship


class IP(db.Model):
    __tablename__ = 'ip_table'
    id = db.Column(db.Integer, primary_key=True, comment="主键ID")
    network_id = db.Column(db.Integer, ForeignKey('networks.id'))
    ip = db.Column(db.String(15), nullable=True, index=True, comment='IP地址')
    status = db.Column(db.String(25), nullable=True, default='used', comment='IP状态')
    hostname = db.Column(db.String(30), nullable=True, default='', comment='主机名')
    mac = db.Column(db.String(15), nullable=True, default='', comment='MAC地址')
    last_discovery_time = db.Column(db.DateTime, default=datetime.datetime.now(), comment='最后发现时间')
    remark = db.Column(db.Text, default='', comment="IP备注")

    def __repr__(self):
        return 'IP: %s, Status: %s, HostName: %s, Mac: %s' % (self.ip, self.status, self.hostname, self.mac)


class Networks(db.Model):
    __tablename__ = 'networks'
    id = db.Column(db.Integer, primary_key=True, comment="主键ID")
    name = db.Column(db.String(30), unique=True, nullable=False, comment='名称')
    network = db.Column(db.String(13), nullable=False, comment='网络号')
    prefix = db.Column(db.Integer, nullable=False, comment="子网掩码")
    gateway = db.Column(db.String(15), default='', comment="网关")
    ip = relationship('IP', backref='network')

    def __repr__(self):
        return 'Network:%s' % self.name


class Time(db.Model):
    __tablename__ = 'time'
    id = db.Column(db.Integer, primary_key=True, comment="主键ID")
    update_time = db.Column(db.DateTime, default=datetime.datetime.now(), comment='更新时间')

    def __repr__(self):
        return 'Update_time:%s' % self.update_time


class Role(db.Model):
    # 定义表名
    __tablename__ = 'roles'
    # 定义列对象
    id = db.Column(db.Integer, primary_key=True, comment="主键ID")
    name = db.Column(db.String(64), comment="角色名称")
    user = db.relationship('User', backref='role')

    # repr()方法显示一个可读字符串,
    def __repr__(self):
        return 'Role:'.format(self.name)


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True, comment="主键ID")
    name = db.Column(db.String(50), nullable=False, comment='姓名')
    username = db.Column(db.String(50), unique=True, nullable=False, comment='用户名')
    email = db.Column(db.String(50), comment='邮箱地址')
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

    def __repr__(self):
        return 'User:'.format(self.username)

视图文件views.py
from . import infra_blu
from flask import request, jsonify
import json
from .utils import common


@infra_blu.route("/apply", methods=['post'])
def apply_machine():
    """申请虚拟机"""
    # 获取前端数据
    res = request.get_data().decode()
    content = json.loads(res)
    print('content:', content)
    # 调用数据处理方法
    analysis = common.App()
    data = analysis.apply_machine(content)
    return jsonify(data)


@infra_blu.route('/profile', methods=['get'])
def profile():
    """现有申请"""
    analysis = common.App()
    data = analysis.conf_search()
    return jsonify(data)


@infra_blu.route('/profile/delete', methods=['post'])
def delete_conf_file():
    """删除配置文件"""
    res = request.get_data().decode()
    content = json.loads(res)
    print('content:', content)
    analysis = common.App()
    data = analysis.delete_file(content)
    return jsonify(data)


@infra_blu.route('/display', methods=['get'])
def display_ip():
    """IP查询"""
    analysis = common.App()
    data = analysis.display()
    return jsonify(data)


@infra_blu.route('/display/update', methods=['get'])
def update_ip():
    """更新IP地址"""
    analysis = common.App()
    data = analysis.update_ip()
    return jsonify(data)


@infra_blu.route('/display/release', methods=['post'])
def release_ip():
    """释放预留或曾用IP地址"""
    res = request.get_data().decode()
    content = json.loads(res)
    print('content:', content)
    msg = 'IP释放成功。'
    status = 'available'
    analysis = common.App()
    data = analysis.modify_status([content['ip']], status, msg)
    return jsonify(data)


@infra_blu.route('/display/addReserved', methods=['post'])
def add_reserved():
    """新增预留ip"""
    res = request.get_data().decode()
    content = json.loads(res)
    print('content:', content)
    analysis = common.App()
    msg = '添加预留IP成功。'
    status = 'reserved'
    tmp_list = content['ip'].split(',')
    ip_list = []
    if bool(tmp_list):
        ip_list = tmp_list
    else:
        ip_list.append(content['ip'])
    data = analysis.modify_status(ip_list, status, msg)
    return jsonify(data)


@infra_blu.route('/display/edit', methods=['post'])
def edit_remark():
    """编辑备注"""
    res = request.get_data().decode()
    content = json.loads(res)
    print('content:', content)
    msg = '备注编辑成功。'
    analysis = common.App()
    data = analysis.edit_remark([content['ip']], [content['remark']], msg)
    return jsonify(data)

5.2.8.2 库文件(infra/utils)
数据处理对象文件common.py
import datetime
import logging
import os
from application import db

from ..utils import config
import json
import time, socket
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.mime.multipart import MIMEMultipart
from ..models import *


class App(object):

    def get_current_ip(self):
        """
        功能: 获取当前用户的访问IP
        返回值: 返回IP地址
        """
        hostname = socket.gethostname()
        ip = socket.gethostbyname(hostname)
        return ip

    def current_time(self):
        """
        功能: 获取当前时间
        返回值: 时间字符串
        """
        date = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
        return date

    def file_list(self):
        """
        功能: 获取虚拟机配置文件名
        返回值: 虚拟机配置文件路径列表
        """
        dirs = os.listdir(config.config_path)
        files = []
        for file in dirs:
            file_path = os.path.join(config.config_path, file)
            files.append(file_path)
        return files

    def clear_csv(self):
        """
        功能: 清空ip_files目录下得所有CSV文件内容
        """
        files = os.listdir(config.ip_file_path)
        try:
            for file in files:
                if file.split('.')[1] == 'csv':
                    file_path = os.path.join(config.ip_file_path, file)
                    file = open(file_path, 'w').close()
        except Exception as e:
            print('ERROR:', e)

    def read_file(self):
        """
        功能: 读取files列表中所有文件的内容
        返回值: 以列表形式返回虚拟机配置文件内容
        """
        config_list = []
        for file in self.file_list():
            with open(file, 'r+') as f:
                res = json.load(f)
                config_list.append(res)
        return config_list

    def get_superUser(self):
        """
        功能: 读取超级用户文件
        返回值:超级用户用户列表
        """
        super_users = User.query.join(Role, isouter=True).filter(Role.id == 1).all()
        super_user_list = []
        for super_user in super_users:
            info = {
                'name': super_user.name,
                'email': super_user.email
            }
            super_user_list.append(info)
        return super_user_list

    def update_ip_to_database(self, key, ip, csv_status, hostname, mac):
        network = Networks.query.filter(Networks.name == key).all()[0]
        ip_tmp = IP.query.filter(IP.ip == ip).first()

        time_tmp = Time.query.first().update_time
        ip_info = {
            'status': csv_status,
            'hostname': hostname,
            'mac': mac
        }
        # 判断数据库中,当前IP是否存在,存在就更新,不存在则插入数据
        if bool(ip_tmp):
            db_status = ip_tmp.status
            try:
                # 更新
                if csv_status == 'using' and db_status in ['using', 'used', 'available']:
                    # if db_status in ['using', 'used', 'available']:
                    ip_info['last_discovery_time'] = time_tmp
                    ip_info['status'] = csv_status
                    IP.query.filter(IP.ip == ip).update(ip_info)
                    # elif db_status == 'reserved':
                    #     ip_info['status'] = db_status
                elif csv_status == 'available' and db_status == 'using':
                    # if db_status == 'using':
                    ip_info['status'] = 'used'
                    IP.query.filter(IP.ip == ip).update(ip_info)
                    # elif db_status in ['reserved', 'used', 'available']:
                    #     ip_info['status'] = db_status

                # IP.query.filter(IP.ip == ip).update(ip_info)
                db.session.commit()

            except Exception as e:
                logging.error(e)
        else:
            # 新增

            db.session.add(
                IP(network_id=network.id, ip=ip, status=csv_status, hostname=hostname, mac=mac,
                   remark='', last_discovery_time=time_tmp))
            db.session.commit()

    def ip_class(self):
        """
        功能: 通过命令行形式运行ipscan工具,扫描结果保存为CSV文件,然后读取文件内IP信息,并分类
        返回值: 返回IP字典。字典含可用IP,已用IP和IP白名单3个IP列表
        """
        files = os.listdir(config.ip_file_path)
        # 遍历CSV文件
        networks = Networks.query.all()
        net_lst = []
        file_name_lst = []
        for network in networks:
            net_lst.append(network.name)
        for file in files:
            file_path = os.path.join(config.ip_file_path, file)
            if file.split('.')[1] == 'csv':
                # 获取网络名
                file_name = file.split('.')[0].split('-')[1]
                file_name_lst.append(file_name)
                # 自动添加networks
                if file_name not in net_lst:
                    f = open(file_path, 'r', encoding='UTF-8')
                    tmp = f.readline()
                    ip_tmp_list = tmp.split(';')[0].split('.')
                    net_tmp = ip_tmp_list[0] + '.' + ip_tmp_list[1] + '.' + ip_tmp_list[2] + '.0'
                    db.session.add(Networks(name=file_name, network=net_tmp, prefix=24))
                    db.session.commit()
                # IP分类
                with open(file_path, 'r+', encoding='UTF-8') as ip_list:
                    net_infos = ip_list.readlines()
                    for net_info in net_infos:
                        info = net_info.split(';')

                        # 可用IP更新到数据库
                        if info[1] == '':
                            # 更新ip到数据库
                            self.update_ip_to_database(file_name, info[0], 'available', '', '')
                        # 更新已用IP到数据库
                        else:
                            self.update_ip_to_database(file_name, info[0], 'using', info[2], info[1])
                    ip_list.close()
        # 清空数据库中无用的networks
        # for net in net_lst:
        #     if net not in key_lst:
        #         Networks.query.filter(Networks.name == net).delete()
        #         db.session.commit()
        return True

    def delete_file(self, content):
        """
        功能: 解析前端数据,获取到文件名后,拼接文件路径,然后删除。
        返回值: 返回删除信息
        """
        delete_flag = content['delete_flag']
        net_name = delete_flag['network'].split(':')[0]
        file_name = delete_flag['systemName'] + '-' + delete_flag[
            'loginName'] + '-' + net_name + '_' + delete_flag['CreateDate'] + '.json'
        dirs = os.listdir(config.config_path)
        file_path = os.path.join(config.config_path, file_name)
        if file_name in dirs:
            try:
                os.remove(file_path)
                msg = {'message': '删除成功!'}
            except Exception as e:
                msg = {'message': '文件不存在,或权限不足!'}
            return msg

    def get_file_name(self, content):
        """
        功能: 生成配置文件名称。
        返回值: 返回生成的文件名file_name
        """
        info = content
        net_name = info['BaseInfos']['network'].split(':')[0]
        date = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
        info['BaseInfos']['CreateDate'] = date
        config.file_name = info['BaseInfos']['systemName'] + '-' + info['BaseInfos'][
            'loginName'] + '-' + net_name + '_' + date + '.json'
        file_name = os.path.join(config.config_path, config.file_name)
        return file_name

    def write_file(self, content):
        """
        功能: 生成虚拟机配置文件
        返回值:
        """

        file_data = json.dumps(content, sort_keys=True,
                               indent=4, separators=(',', ': '))
        f = open(self.get_file_name(content), 'w+')
        f.write(file_data)
        f.close()

    def user_white_list(self):
        """
        功能: 读取用户白名单文件
        返回值:白名单用户列表
        """
        with open(config.user_file_path, 'r+') as f:
            user_list = f.read().splitlines()
        user_white_list = {
            'user_white_list': user_list
        }
        return user_white_list

    def get_files_ctime(self):
        """
        功能: 获取IP文件的修改时间,判断文件修改时间与当前时间是否在5分钟内,不在则将文件名放入列表update_failed_files中
        返回值: 更新失败的文件列表update_failed_files
        """
        files = os.listdir(config.ip_file_path)
        update_failed_files = []
        for file in files:
            if file.split('.')[1] == 'csv':
                file_path = os.path.join(config.ip_file_path, file)
                ctime = os.path.getctime(file_path)
                res = ctime - time.time()
                file_name = file.split('.')[0].split('-')[1]
                if abs(res) > 300:
                    update_failed_files.append(file_name)
        return update_failed_files

    def send_mail(self):
        """
        功能: 发送邮件给管理员
        """
        msg = '基础设施管理平台IP更新时,以下网段IP信息未更新: ' + str(self.get_files_ctime())
        mail_list = []
        user_list = []
        for user in self.get_superUser():
            mail = user['email']
            mail_list.append(mail)
            user_list.append(user['name'])
        subject = '【告警】基础设施管理平台'
        self.mailto(receivers=mail_list, subject=subject, msg=msg, sender='infra',
                    passwd='Aa753951!')
        msg = "已向以下管理员发送邮件:" + str(user_list)
        logging.info(msg)

    def update_ip(self):
        """
        功能: 判断IP文件修改时间是否与当前时间差在5分钟内
        返回值: 在则返回True 不在则返回False
        """

        # 查询数据库中时间
        last_time_query = Time.query.first()

        # 获取当前时间
        cur_time = datetime.datetime.now()
        current_time_tmp = cur_time.strftime('%Y-%m-%d %H:%M:%S')
        current_time = datetime.datetime.strptime(current_time_tmp, '%Y-%m-%d %H:%M:%S')

        # 数据库中查不到时间信息,说明是第一次更新,需要插入时间字符串
        if bool(last_time_query) == False:

            # 更新CSV文件
            logging.info(str(time.time()) + 'Start to update csv.')
            cmd = 'ansible-playbook %s' % config.yml_file_path
            os.system(cmd)
            logging.info(str(time.time()) + 'Update csv complete.')

            # 将当前时间写入数据库
            try:
                db.session.add(Time(update_time=cur_time))
                db.session.commit()
                logging.info(str(time.time()) + 'Update DataBase complete.')
            except Exception as e:
                logging.error('写入数据库失败', e)

            if len(self.display()['update_failed_files']) > 0:
                try:
                    self.send_mail()
                except Exception as e:
                    logging.error('邮件发送失败', e)
            # ip_class()对IP进行分类(可用,已用)
            try:
                self.ip_class()
            except Exception as e:
                logging.error('ipclass', e)
            return self.display()
        else:
            last_time_query = last_time_query.update_time
            yes_time = last_time_query + datetime.timedelta()
            yes_time_tmp = yes_time.strftime('%Y-%m-%d %H:%M:%S')
            last_time = datetime.datetime.strptime(yes_time_tmp, '%Y-%m-%d %H:%M:%S')
            tmp = current_time - last_time

            if (tmp.days * 24 * 60 * 60 + tmp.seconds) < 300:
                return 'ip信息无需频繁更新,请稍后再试!'
            else:

                # 更新CSV文件
                cmd = 'ansible-playbook %s' % config.yml_file_path
                os.system(cmd)

                # 更新时间
                time_tmp = Time.query.first()
                time_tmp.update_time = cur_time
                db.session.commit()

                # 存在更新失败文件时发送邮件
                if len(self.display()['update_failed_files']) > 0:
                    try:
                        # self.send_mail()
                        print('send mail')
                    except Exception as e:
                        logging.error('邮件发送失败.', e)
                try:
                    self.ip_class()
                except Exception as e:
                    logging.error('ipclass', e)
                return self.display()

    def apply_machine(self, content):
        # 配置信息空值给与默认值
        for k, v in config.app_info_list.items():
            if k not in content:
                content[k] = v
            else:
                if k == 'groupInfos':
                    for i in range(0, len(content[k])):
                        for idx, vl in v.items():
                            if idx not in content[k][i]:
                                content[k][i][idx] = vl
        if self.get_file_name(content) in self.file_list():
            # 写入JSON文件
            msg = "此配置已存在,请确认是否配置文件名重复!"
            code = 403
            print(msg, code)
            return msg, code
        else:
            self.write_file(content)
            return 'ok'

    def conf_search(self):
        files = self.file_list()
        data = {
            'info': self.read_file(),
            'superUser': self.get_superUser(),
            'infra': self.get_current_ip()
        }
        print(self.current_time(), self.get_superUser())
        return data
    
    
    def mailto(self, receivers, subject, msg, sender, passwd):
        """
        功能: 邮件发送方法
        返回值: null
        """
        sender = sender
        receivers = receivers  # 接收邮件,可设置为你的QQ邮箱或者其他邮箱

        # 创建一个带附件的实例
        message = MIMEMultipart()
        subject = subject
        message['Subject'] = Header(subject, 'utf-8')
        message["From"] = sender
        message["To"] = ';'.join(receivers)
        # 邮件正文内容
        mail_msg = msg
        message.attach(MIMEText(mail_msg, 'html', 'utf-8'))
        try:
            smtpObj = smtplib.SMTP('10.86.228.5', 25)
            smtpObj.login(sender, passwd)

            smtpObj.sendmail(sender + '@163.com', receivers, message.as_string())
            logging.info("邮件发送成功。")
        except smtplib.SMTPException as e:
            logging.error(e)

    def replace_null(self, str):
        """
        功能: 前端展示数据库中展示的数据时,将空数据替换成‘/’
        返回值: str
        """
        if bool(str):
            return str
        else:
            str = '/'
            return str

    def format_ip_info(self, infos):
        """
        功能: 格式化ip信息
        返回值: ip信息列表info_list
        """
        info_list = []
        for info in infos:
            yes_time = info.last_discovery_time + datetime.timedelta()
            time_tmp = yes_time.strftime('%Y-%m-%d %H:%M:%S')
            ip_info = {
                'ip': self.replace_null(info.ip),
                'hostname': self.replace_null(info.hostname.strip('
')),
                'mac': self.replace_null(info.mac),
                'last_discovery_time': time_tmp,
                'remark': self.replace_null(info.remark)
            }
            info_list.append(ip_info)
        return info_list

    def display(self):
        """
        功能: 查询数据库数据(ip信息和更新时间),将数据返回前端展示
        返回值: data
        """
        # 上一次更新的时间,当last_time为FALSE,首次需要写入时间
        last_time = Time.query.first()
        if bool(last_time) == True:
            last_time = last_time.update_time
            yes_time = last_time + datetime.timedelta()
            last_time = yes_time.strftime('%Y-%m-%d %H:%M:%S')
        # 查询网段列表
        network_list = Networks.query.all()
        ip_dict = {}
        # 循环网段,查询不同网段下得ip信息
        for network in network_list:
            # 获取网段名
            network_name = network.name

            # 查询使用中的ip信息
            usings = IP.query.filter(IP.status == 'using').join(Networks, isouter=True).filter(
                Networks.name == network_name).all()
            using_list = self.format_ip_info(usings)

            # 查询曾用ip信息
            useds = IP.query.filter(IP.status == 'used').join(Networks, isouter=True).filter(
                Networks.name == network_name).all()
            used_list = self.format_ip_info(useds)

            # 查询可用ip信息
            availables = IP.query.filter(IP.status == 'available').join(Networks, isouter=True).filter(
                Networks.name == network_name).all()
            available_list = self.format_ip_info(availables)

            # 查询预留ip信息
            reserveds = IP.query.filter(IP.status == 'reserved').join(Networks, isouter=True).filter(
                Networks.name == network_name).all()
            reserved_list = self.format_ip_info(reserveds)

            ip_dict[network_name] = {
                'using_list': using_list,
                'used_list': used_list,
                'available_list': available_list,
                'reserved_list': reserved_list,
            }
        # 点击更新数据按钮时返回更新失败文件
        config.update_failed_files = self.get_files_ctime()
        data = {
            'ip_dict': ip_dict,
            'update_time': last_time,
            'update_failed_files': config.update_failed_files
        }
        return data

    def modify_status(self, ip_list, status, msg):
        for ip in ip_list:
            try:
                IP.query.filter(IP.ip == ip).update({'status': status})
                db.session.commit()
                logging.info(msg)
            except Exception as e:
                logging.error(e)
        return self.display()

    def edit_remark(self, ip, remark, msg):
        try:
            IP.query.filter(IP.ip == ip).update({'remark': remark})
            db.session.commit()
            logging.info(msg)
        except Exception as e:
            logging.error(e)
        return self.display()
常量配置文件 config.py
config_path = r'../vmware_config'
data = []
yml_file_path = '/home/zhuhg/ipscan.yml'
user_file_path = '../../../ip_files/user_white_list.txt'
super_user_file_path = '../../../ip_files/super_user.txt'
response = {}
file_name = ''
update_failed_files = []
ip_file_path = '../ip_files'
time_path = '../../../ip_files/time.txt'
# 数据初始化
groupInfos = {
    'JDK8': False,
    'JDK11': False,
    'nginx': False,
    'skywalking': False,
    'consulclient': False,
    'hostName': ' ',
    'appName': ' ',
    'CPU': '0',
    'MEM': '0',
    'disk_size': '0',
    'count': '0 '
}
consulServerInfos = {
    'count': '0',
    'isInstall': False
}
kafkaInfos = {
    'count': '0',
    'isInstall': False
}
dockerInfos = {
    'count': '0',
    'isInstall': False
}
redisInfos = {
    'count': '0',
    'isInstall': False
}
app_info_list = {
    'consulServerInfos': consulServerInfos,
    'kafkaInfos': kafkaInfos,
    'redisInfos': redisInfos,
    'dockerInfos': dockerInfos,
    "groupInfos": groupInfos
}
query_dict = 'flag'

六、KeyCloak单点登录

6.1 KeyCloak介绍

网络时代,人们对安全的要求越来越高,相应的就制定出来一些规范,比如Oauth2,OpenID等等,为了实现这些规范,设计了keycloak。对于对安全方面不是很熟悉的人可以直接使用keycloak去实现用户认证,授权等一系列操作,不需要开发自己的安全方面的东西。

在本项目中由于使用的是公司前端开发封装好的组件,此处就不赘述。以下简单介绍一下个人对KeyCloak测试时的使用配置过程,以供了解。

6.2 KeyCloak安装

安装KeyCloak方法很多,在官网提供了很多种方式以供选择。官网地址:https://www.keycloak.org/getting-started

image-20210806091145216

由于公司服务器所在网络无法安装docker,k8s等,因此我在测试时是在本地PC安装Windows Docker的方式安装的。此处不赘述安装过程,以上地址中有完整安装过程。

安装完成后,浏览器访问地址:http://localhost:8080/auth/,若出现以下页面则说明配置成功。

image-20210806091717174

6.3 KeyCloak使用

6.3.1 创建域。

首先,点击导航栏的Add realm创建一个域,然后选择刚刚新建的域,如Lalala。然后可以对域进行配置,也可使用默认设置。

image-20210806092142902

6.3.2 创建客户端

点击导航栏Clients进入客户端页面,点击右上角Create按钮进行创建。

image-20210806092802838

简单配置客户端lalala,Access Type有三个值分别对应不同的访问类型

confidential:适用于服务端应用,且需要浏览器登录以及需要通过密钥获取access token的场景。典型的使用场景就是服务端渲染的web系统。
public:适用于客户端应用,且需要浏览器登录的场景。典型的使用场景就是前端web系统,包括采用vue、react实现的前端项目等。
bearer-only:适用于服务端应用,不需要浏览器登录,只允许使用bearer token请求的场景。典型的使用场景就是restful api
Valid Redirect URIs 就是你允许访问你 keycloak 的地址

编辑客户端,解决跨域问题。此处你应该在Web起源(Web origin)中填入你客户端的URL地址。我这里因为是测试效果,所以填了*,但是这是不安全的,建议你按需要配置

image-20210806093149876

6.3.3 创建角色和用户

点击Roles创建角色,点击Users创建用户并设置用户密码,然后给用户分配角色infra

image-20210806094958126

6.4 Vue配置KeyCloak实现单点登录

6.4.1 安装@dsb-norge/vue-keycloak-js

使用npm安装

npm install @dsb-norge/vue-keycloak-js

image-20210806095535271

6.4.2 vue项目的main.js中引入

import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App'
import router from './router'
import axios from 'axios'
import vueKeycloak from '@dsb-norge/vue-keycloak-js'

Vue.use(ElementUI)
Vue.config.productionTip = false
Vue.prototype.$axios = axios

Vue.use(vueKeycloak, {
  init: {
    onLoad: 'login-required'
  },
  config: {

    url: 'http://localhost:8080/auth/', //keycloak校验地址
    realm: 'lalala',			//创建的域名称
    clientId: 'lalala'			//创建的客户端名称

  },
  onReady: (keycloak) => {
    keycloak.loadUserProfile().success((data) => {
      console.log(this.$keycloak)   //this.$keycloak是keycloak提供的一个全局对象,通过这个对象可以实现获取用户名,注销登录等操作
      console.log(data)
    })
    console.log(keycloak)
  }
})

运行项目,浏览器输入项目地址,会转到keycloak登录界面

image-20210806100327332

七、上线部署

7.1 打包文件上传服务器

7.1.1 vue前端静态文件打包

在命令行中切换到vue项目目录下,运行以下命令进行打包

npm run build

image-20210806100951611

运行完成后,出现Build complete说明打包成功,此时项目目录下多出一个dist文件(默认情况下)

image-20210806101145800

7.1.2 生成环境依赖文件requirements.txt

运行以下命令,生成依赖文件,运行完成后可以在相应目录下查看到requirements.txt文件

pip3 freeze > requirements.txt

image-20210806101427896

image-20210806101543606

7.1.3 安装Python3

首先确定服务器上是否已安装Python3

image-20210806111332000

若没有安装,则把Python3装上。具体安装流程不演示,自行安装。

7.1.4 所有项目文件打包上传服务器

以上步骤都做完后,将后端项目打包压缩,前端项目只需要讲dist目录打包压缩即可。然后前后端项目上传服务器(路径自选)解压出来。

目录结构如下:

image-20210806102011057

7.1.5 一键安装环境依赖

sudo pip3 install -r requirements.txt

7.2 前端部署

7.2.1 安装Nginx

具体安装流程不做解释,此处只展示当前服务器中已安装的Nginx版本

image-20210806104814648

7.2.2 配置Nginx

vim 打开Nginx配置文件/etc/nginx/conf.d/default.conf,配置如下:

#前端服务器代理
server {
    listen       80;
    server_name  www.web.xom;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root /home/zhuhg/infra/infra_web/dist; //前端项目地址
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
        if ($request_filename ~* .*.(?:htm|html)$) {
           add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
        }
    }


    location ^~ /ssoCfg {
       #改成ssoCfg.json放置的位置,如果keycloak是按照本文中配置使用,则无需配置,此处只配合公司sso使用。
       alias /home/zhuhg/infra/ssoCfg/ssoCfg-dev.json;
       default_type application/json;

       add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
       add_header Access-Control-Allow-Origin *;
       add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
       add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
       proxy_set_header X-Forward-For $remote_addr;
       if ($request_method = 'OPTIONS') {
          return 204;
       }
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
#后端服务器代理
 server {
        listen          9000;
        server_name     www.server.com;
        location / {
            uwsgi_pass 0.0.0.0:8000;    //代理转发到uwsgi
            include /etc/nginx/uwsgi_params;
        }

7.3 后端部署

7.3.1 uwsgi介绍

WSGI是Web服务器网关接口。它是一个规范,描述了Web服务器如何与Web应用程序通信,以及Web应用程序如何链接在一起以处理一个请求,(接收请求,处理请求,响应请求)
基于wsgi运行的框架有Bottle,Django,Flask,用于解析动态HTTP请求
支持WSGI的服务器
    wsgiref
        python自带的web服务器
    Gunicorn
        用于linux的 python wsgi Http服务器,常用于各种django,flask结合部署服务器。
    mode_wsgi
        实现了Apache与wsgi应用程序的结合
    uWSGI
        C语言开发,快速,自我修复,开发人员友好的WSGI服务器,用于Python Web应用程序的专业部署和开发。

在部署python程序web应用程序时,可以根据性能的需求,选择合适的wsgi server,不同的wsgi server区别在于并发支持上,有单线程,多进程,多线程,协程的区别,其功能还是近似,无非是请求路由,执行对应的函数,返回处理结果。

7.3.2 安装uwsgi

安装uwsgi

sudo pip3 install  uwsgi 

检查安装版本

image-20210806111642617

7.3.3 配置uwsgi

uwsgi配置详解:

[uwsgi]# 对外提供 http 服务的端口,如果你使用了nginx,做反向代理,必须填写socket链接,而不是http参数
http = :8000

#the local unix socket file than commnuincate to Nginx 用于和 nginx 进行数据交互的端口
socket = 127.0.0.1:8001

# the base directory (full path) django 程序的主目录
chdir =/home/opadm/mms_webserver/src

# Django's wsgi file
wsgi-file =src/wsgi.py

# maximum number of worker processes
processes = 100

#thread numbers startched in each worker process
threads = 10

#一个高阶的cheap模式,在启动的时候只会分配n个工作进程,并使用自适应算法启动新的进程
cheaper = 10

#在经过sec秒的不活跃状态的进程会被销毁(进入了cheap模式),并最少保留cheaper指定的进程数
idle = 3600

#monitor uwsgi status 通过该端口可以监控 uwsgi 的负载情况
stats = 127.0.0.1:9000

#设置一个请求的超时时间(秒),如果一个请求超过了这个时间,则请求被丢弃
harakiri = 60
#当一个请求被harakiri杀掉会,会输出一条日志
harakiri-verbose = true

#开启内存使用情况报告
memory-report = true

#设置平滑的重启(直到处理完接收到的请求)的长等待时间(秒)
reload-mercy = 10

#设置工作进程使用虚拟内存超过N MB就回收重启
reload-on-as= 1024

#自动给进程命名
auto-procname = true

#为进程指定前缀
procname-prefix-spaced = xc-mms

#设置工作进程每处理N个进程就会被回收重启
max-requests=500000

#设置工作进程使用物理内存超过N MB就回收重启
reload-on-rss=100

#设置socket超时时间,默认4秒
socket-timeout=10

#限制http请求体的大小(Bytes)
limit-post=4096

# clear environment on exit
vacuum = true

#不记录request日志,只记录错误日志
disable-logging = true

#将日志打印到syslog上
#log-syslog = true

# 后台运行,并输出日志
daemonize = /home/opadm/log/uwsgi.logstats=./uwsgi.status

实际uwsgi配置如下:

[uwsgi]
# uwsgi 启动时所使用的地址与端口,nginx代理的时候需要转发到该地址
socket = 0.0.0.0:9000
#python环境目录
#home = /home/zhuhg/xnj/venv/bin
#虚拟环境
home =/home/zhuhg/infra/venv
#指向后端项目根目录
chdir = /home/zhuhg/infra/infra_ser
#python项目启动程序文件,此处为flask入口文件app.py
wsgi-file = /home/zhuhg/infra/infra_ser/app.py
#python程序内用于启动的application变量名
callable = app
#处理器数
processes = 2
#线程数
threads = 2
#状态监测地址
stats = 127.0.0.1:50001
#设置uwsgi包解析的内部缓存区大小。默认4k
buffer-size = 32768
vacuum = true
master = true

daemonize = /home/zhuhg/infra/uwsgi.log
stats = /home/zhuhg/infra/uwsgi.status

原文地址:https://www.cnblogs.com/zhface/p/15211009.html