James Iter 7 年之前
父節點
當前提交
7bea988031

+ 8 - 0
jimvc/__init__.py

@@ -67,6 +67,10 @@ from jimvc.api.host_performance import blueprints as host_performance_blueprints
 from jimvc.api.dashboard import blueprint as dashboard_blueprint
 from jimvc.api.dashboard import blueprint as dashboard_blueprint
 from jimvc.api.dashboard import blueprints as dashboard_blueprints
 from jimvc.api.dashboard import blueprints as dashboard_blueprints
 from jimvc.api.about import blueprint as about_blueprint
 from jimvc.api.about import blueprint as about_blueprint
+from jimvc.api.project import blueprint as project_blueprint
+from jimvc.api.project import blueprints as project_blueprints
+from jimvc.api.service import blueprint as service_blueprint
+from jimvc.api.service import blueprints as service_blueprints
 
 
 from jimvc.views.error_pages import *
 from jimvc.views.error_pages import *
 from jimvc.views.config import blueprint as view_config_blueprint
 from jimvc.views.config import blueprint as view_config_blueprint
@@ -272,6 +276,10 @@ try:
     app.register_blueprint(dashboard_blueprint)
     app.register_blueprint(dashboard_blueprint)
     app.register_blueprint(dashboard_blueprints)
     app.register_blueprint(dashboard_blueprints)
     app.register_blueprint(about_blueprint)
     app.register_blueprint(about_blueprint)
+    app.register_blueprint(project_blueprint)
+    app.register_blueprint(project_blueprints)
+    app.register_blueprint(service_blueprint)
+    app.register_blueprint(service_blueprints)
 
 
     app.register_blueprint(view_config_blueprint)
     app.register_blueprint(view_config_blueprint)
     app.register_blueprint(view_misc_blueprint)
     app.register_blueprint(view_misc_blueprint)

+ 20 - 2
jimvc/api/guest.py

@@ -16,7 +16,7 @@ from flask import Blueprint, url_for, request
 
 
 from jimvc.api.base import Base
 from jimvc.api.base import Base
 from jimvc.models.initialize import dev_table
 from jimvc.models.initialize import dev_table
-from jimvc.models import app_config, GuestState
+from jimvc.models import app_config, GuestState, Service
 from jimvc.models import DiskState, Host
 from jimvc.models import DiskState, Host
 from jimvc.models import Database as db
 from jimvc.models import Database as db
 from jimvc.models import Config
 from jimvc.models import Config
@@ -83,6 +83,11 @@ def r_create():
             Rules.SSH_KEYS_ID.value
             Rules.SSH_KEYS_ID.value
         )
         )
 
 
+    if 'service_id' in request.json:
+        args_rules.append(
+            Rules.SERVICE_ID.value
+        )
+
     try:
     try:
         ret = dict()
         ret = dict()
         ret['state'] = ji.Common.exchange_state(20000)
         ret['state'] = ji.Common.exchange_state(20000)
@@ -149,6 +154,11 @@ def r_create():
             for row in rows:
             for row in rows:
                 ssh_keys.append(row['public_key'])
                 ssh_keys.append(row['public_key'])
 
 
+        # 确保目标 服务组 存在
+        service = Service()
+        service.id = request.json.get('service_id', 1)
+        service.get()
+
         bandwidth = request.json.get('bandwidth')
         bandwidth = request.json.get('bandwidth')
         bandwidth_unit = request.json.get('bandwidth_unit')
         bandwidth_unit = request.json.get('bandwidth_unit')
 
 
@@ -222,6 +232,7 @@ def r_create():
                 chosen_host = available_hosts_mapping_by_node_id[node_id]
                 chosen_host = available_hosts_mapping_by_node_id[node_id]
 
 
             guest.node_id = chosen_host['node_id']
             guest.node_id = chosen_host['node_id']
+            guest.service_id = service.id
 
 
             guest_xml = GuestXML(host=chosen_host, guest=guest, disk=disk, config=config,
             guest_xml = GuestXML(host=chosen_host, guest=guest, disk=disk, config=config,
                                  os_type=os_template_profile.os_type)
                                  os_type=os_template_profile.os_type)
@@ -1094,7 +1105,8 @@ def r_distribute_count():
 def r_update(uuid):
 def r_update(uuid):
 
 
     args_rules = [
     args_rules = [
-        Rules.UUID.value
+        Rules.UUID.value,
+        Rules.SERVICE_ID.value
     ]
     ]
 
 
     if 'remark' in request.json:
     if 'remark' in request.json:
@@ -1102,6 +1114,11 @@ def r_update(uuid):
             Rules.REMARK.value,
             Rules.REMARK.value,
         )
         )
 
 
+    if 'service_id' in request.json:
+        args_rules.append(
+            Rules.SERVICE_ID.value,
+        )
+
     if args_rules.__len__() < 2:
     if args_rules.__len__() < 2:
         ret = dict()
         ret = dict()
         ret['state'] = ji.Common.exchange_state(20000)
         ret['state'] = ji.Common.exchange_state(20000)
@@ -1116,6 +1133,7 @@ def r_update(uuid):
         guest.get_by('uuid')
         guest.get_by('uuid')
 
 
         guest.remark = request.json.get('remark', guest.label)
         guest.remark = request.json.get('remark', guest.label)
+        guest.service_id = request.json.get('service_id', guest.service_id)
 
 
         guest.update()
         guest.update()
         guest.get()
         guest.get()

+ 159 - 0
jimvc/api/project.py

@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+import json
+from flask import Blueprint
+from flask import request
+
+from jimvc.api.base import Base
+from jimvc.models import Utils
+from jimvc.models import Rules
+from jimvc.models import Project
+
+
+__author__ = 'James Iter'
+__date__ = '2018/10/7'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_project',
+    __name__,
+    url_prefix='/api/project'
+)
+
+blueprints = Blueprint(
+    'api_projects',
+    __name__,
+    url_prefix='/api/projects'
+)
+
+
+project_base = Base(the_class=Project, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_create():
+
+    args_rules = [
+        Rules.NAME.value
+    ]
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.value,
+        )
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, request.json)
+
+        project = Project()
+        project.name = request.json.get('name', None)
+        project.description = request.json.get('description', '')
+
+        project.create()
+        project.get_by('create_time')
+
+        ret['data'] = project.__dict__
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_update(_id):
+
+    project = Project()
+
+    args_rules = [
+        Rules.ID.value
+    ]
+
+    if 'name' in request.json:
+        args_rules.append(
+            Rules.NAME.value,
+        )
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.value,
+        )
+
+    if args_rules.__len__() < 2:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    request.json['id'] = _id
+
+    try:
+        ji.Check.previewing(args_rules, request.json)
+        project.id = int(request.json.get('id'))
+
+        project.get()
+        project.name = request.json.get('name', project.name)
+        project.description = request.json.get('description', project.description)
+
+        project.update()
+        project.get()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = project.__dict__
+        return ret
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_get(ids):
+    return project_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+
+
+@Utils.dumps2response
+def r_get_by_filter():
+    return project_base.get_by_filter()
+
+
+@Utils.dumps2response
+def r_content_search():
+    return project_base.content_search()
+
+
+@Utils.dumps2response
+def r_delete(ids):
+
+    args_rules = [
+        Rules.IDS.value
+    ]
+
+    try:
+        ji.Check.previewing(args_rules, {'ids': ids})
+
+        project = Project()
+
+        # 检测所指定的 项目 都存在
+        for _id in ids.split(','):
+            project.id = int(_id)
+            project.get()
+
+        # 执行删除操作
+        for _id in ids.split(','):
+            project.id = int(_id)
+            project.get()
+            project.delete()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+

+ 167 - 0
jimvc/api/service.py

@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+import json
+from flask import Blueprint
+from flask import request
+
+from jimvc.api.base import Base
+from jimvc.models import Utils
+from jimvc.models import Rules
+from jimvc.models import Service
+
+
+__author__ = 'James Iter'
+__date__ = '2018/10/7'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_service',
+    __name__,
+    url_prefix='/api/service'
+)
+
+blueprints = Blueprint(
+    'api_services',
+    __name__,
+    url_prefix='/api/services'
+)
+
+
+service_base = Base(the_class=Service, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_create():
+
+    args_rules = [
+        Rules.PROJECT_ID.value,
+        Rules.NAME.value
+    ]
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.value,
+        )
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, request.json)
+
+        service = Service()
+        service.project_id = request.json.get('project_id', None)
+        service.name = request.json.get('name', None)
+        service.description = request.json.get('description', '')
+
+        service.create()
+        service.get_by('create_time')
+
+        ret['data'] = service.__dict__
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_update(_id):
+
+    service = Service()
+
+    args_rules = [
+        Rules.ID.value
+    ]
+
+    if 'project_id' in request.json:
+        args_rules.append(
+            Rules.PROJECT_ID.value,
+        )
+
+    if 'name' in request.json:
+        args_rules.append(
+            Rules.NAME.value,
+        )
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.value,
+        )
+
+    if args_rules.__len__() < 2:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    request.json['id'] = _id
+
+    try:
+        ji.Check.previewing(args_rules, request.json)
+        service.id = int(request.json.get('id'))
+
+        service.get()
+        service.project_id = request.json.get('project_id', service.project_id)
+        service.name = request.json.get('name', service.name)
+        service.description = request.json.get('description', service.description)
+
+        service.update()
+        service.get()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = service.__dict__
+        return ret
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_get(ids):
+    return service_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+
+
+@Utils.dumps2response
+def r_get_by_filter():
+    return service_base.get_by_filter()
+
+
+@Utils.dumps2response
+def r_content_search():
+    return service_base.content_search()
+
+
+@Utils.dumps2response
+def r_delete(ids):
+
+    args_rules = [
+        Rules.IDS.value
+    ]
+
+    try:
+        ji.Check.previewing(args_rules, {'ids': ids})
+
+        service = Service()
+
+        # 检测所指定的 项目 都存在
+        for _id in ids.split(','):
+            service.id = int(_id)
+            service.get()
+
+        # 执行删除操作
+        for _id in ids.split(','):
+            service.id = int(_id)
+            service.get()
+            service.delete()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+

+ 35 - 3
jimvc/api_route_table.py

@@ -3,9 +3,25 @@
 
 
 
 
 from jimvc.models import add_rule_api
 from jimvc.models import add_rule_api
-from jimvc.api import config, os_template_profile, snapshot, os_template_initialize_operate, user, ssh_key, \
-    os_template_image, guest_performance, disk, log, os_template_initialize_operate_set, dashboard, guest, host, \
-    host_performance, about
+from jimvc.api import config
+from jimvc.api import os_template_profile
+from jimvc.api import snapshot
+from jimvc.api import os_template_initialize_operate
+from jimvc.api import user
+from jimvc.api import ssh_key
+from jimvc.api import os_template_image
+from jimvc.api import guest_performance
+from jimvc.api import disk
+from jimvc.api import log
+from jimvc.api import os_template_initialize_operate_set
+from jimvc.api import dashboard
+from jimvc.api import guest
+from jimvc.api import host
+from jimvc.api import host_performance
+from jimvc.api import about
+from jimvc.api import project
+from jimvc.api import service
+
 
 
 __author__ = 'James Iter'
 __author__ = 'James Iter'
 __date__ = '2017/03/30'
 __date__ = '2017/03/30'
@@ -168,6 +184,22 @@ add_rule_api(snapshot.blueprint, '/_convert_to_os_template_image/<snapshot_id>/<
              api_func='snapshot.r_convert_to_os_template_image', methods=['PUT'])
              api_func='snapshot.r_convert_to_os_template_image', methods=['PUT'])
 add_rule_api(snapshot.blueprints, '/_show', api_func='snapshot.r_show', methods=['GET'])
 add_rule_api(snapshot.blueprints, '/_show', api_func='snapshot.r_show', methods=['GET'])
 
 
+# 项目操作
+add_rule_api(project.blueprint, '', api_func='project.r_create', methods=['POST'])
+add_rule_api(project.blueprint, '/<_id>', api_func='project.r_update', methods=['PATCH'])
+add_rule_api(project.blueprints, '/<ids>', api_func='project.r_delete', methods=['DELETE'])
+add_rule_api(project.blueprints, '/<ids>', api_func='project.r_get', methods=['GET'])
+add_rule_api(project.blueprints, '', api_func='project.r_get_by_filter', methods=['GET'])
+add_rule_api(project.blueprints, '/_search', api_func='project.r_content_search', methods=['GET'])
+
+# 服务组操作
+add_rule_api(service.blueprint, '', api_func='service.r_create', methods=['POST'])
+add_rule_api(service.blueprint, '/<_id>', api_func='service.r_update', methods=['PATCH'])
+add_rule_api(service.blueprints, '/<ids>', api_func='service.r_delete', methods=['DELETE'])
+add_rule_api(service.blueprints, '/<ids>', api_func='service.r_get', methods=['GET'])
+add_rule_api(service.blueprints, '', api_func='service.r_get_by_filter', methods=['GET'])
+add_rule_api(service.blueprints, '/_search', api_func='service.r_content_search', methods=['GET'])
+
 # Guest 性能查询
 # Guest 性能查询
 add_rule_api(guest_performance.blueprint, '/cpu_memory',
 add_rule_api(guest_performance.blueprint, '/cpu_memory',
              api_func='guest_performance.r_cpu_memory_get_by_filter', methods=['GET'])
              api_func='guest_performance.r_cpu_memory_get_by_filter', methods=['GET'])

+ 6 - 1
jimvc/models/__init__.py

@@ -46,6 +46,11 @@ from guest import (
     GuestMigrateInfo
     GuestMigrateInfo
 )
 )
 
 
+from project import (
+    Project,
+    Service
+)
+
 from ssh_key import (
 from ssh_key import (
     SSHKey
     SSHKey
 )
 )
@@ -133,6 +138,6 @@ __all__ = [
     'OSTemplateInitializeOperate', 'EventProcessor', 'ResponseState', 'GuestCPUMemory', 'GuestTraffic', 'GuestDiskIO',
     'OSTemplateInitializeOperate', 'EventProcessor', 'ResponseState', 'GuestCPUMemory', 'GuestTraffic', 'GuestDiskIO',
     'HostCPUMemory', 'HostTraffic', 'HostDiskUsageIO', 'Host', 'Snapshot', 'SnapshotDiskMapping', 'OSTemplateImageKind',
     'HostCPUMemory', 'HostTraffic', 'HostDiskUsageIO', 'Host', 'Snapshot', 'SnapshotDiskMapping', 'OSTemplateImageKind',
     'GuestMigrateInfo', 'GuestCollectionPerformanceDataKind', 'HostCollectionPerformanceDataKind', 'StorageMode',
     'GuestMigrateInfo', 'GuestCollectionPerformanceDataKind', 'HostCollectionPerformanceDataKind', 'StorageMode',
-    'OSTemplateInitializeOperateKind', 'dev_table'
+    'OSTemplateInitializeOperateKind', 'dev_table', 'Project', 'Service'
 ]
 ]
 
 

+ 3 - 1
jimvc/models/guest.py

@@ -32,6 +32,7 @@ class Guest(ORM):
         self.status = GuestState.no_state.value
         self.status = GuestState.no_state.value
         self.progress = 0
         self.progress = 0
         self.node_id = None
         self.node_id = None
+        self.service_id = 1
         self.cpu = None
         self.cpu = None
         self.memory = None
         self.memory = None
         self.bandwidth = 0
         self.bandwidth = 0
@@ -51,13 +52,14 @@ class Guest(ORM):
             'status': FilterFieldType.INT.value,
             'status': FilterFieldType.INT.value,
             'remark': FilterFieldType.STR.value,
             'remark': FilterFieldType.STR.value,
             'node_id': FilterFieldType.INT.value,
             'node_id': FilterFieldType.INT.value,
+            'service_id': FilterFieldType.INT.value,
             'ip': FilterFieldType.STR.value,
             'ip': FilterFieldType.STR.value,
             'bandwidth': FilterFieldType.INT.value
             'bandwidth': FilterFieldType.INT.value
         }
         }
 
 
     @staticmethod
     @staticmethod
     def get_allow_update_keywords():
     def get_allow_update_keywords():
-        return ['remark', 'cpu', 'memory', 'bandwidth', 'network', 'manage_network', 'vnc_password']
+        return ['remark', 'cpu', 'memory', 'bandwidth', 'network', 'manage_network', 'vnc_password', 'service_id']
 
 
     @staticmethod
     @staticmethod
     def get_allow_content_search_keywords():
     def get_allow_content_search_keywords():

+ 72 - 0
jimvc/models/project.py

@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+
+from filter import FilterFieldType
+from orm import ORM
+
+
+__author__ = 'James Iter'
+__date__ = '2018/10/7'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+class Project(ORM):
+
+    _table_name = 'project'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(Project, self).__init__()
+        self.id = 0
+        self.name = ''
+        self.description = ''
+        self.create_time = ji.Common.tus()
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'name': FilterFieldType.STR.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return []
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['name']
+
+
+class Service(ORM):
+
+    _table_name = 'service'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(Service, self).__init__()
+        self.id = 0
+        self.project_id = 0
+        self.name = ''
+        self.description = ''
+        self.create_time = ji.Common.tus()
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'project_id': FilterFieldType.INT.value,
+            'name': FilterFieldType.STR.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return ['project_id']
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['name']

+ 4 - 0
jimvc/models/rules.py

@@ -123,3 +123,7 @@ class Rules(Enum):
 
 
     TOKEN = (basestring, 'token')
     TOKEN = (basestring, 'token')
 
 
+    PROJECT_ID = (int, 'project_id')
+    PROJECTS_ID = (REG_NUMBERS, 'projects_id')
+    SERVICE_ID = (int, 'service_id')
+    SERVICES_ID = (REG_NUMBERS, 'services_id')

+ 380 - 4
jimvc/themes/default/templates/guests_show.html

@@ -80,6 +80,57 @@
 
 
         .btn-ability-line {
         .btn-ability-line {
         }
         }
+
+        .project {
+            position: relative;
+            float: left;
+            display: block;
+            margin: 0 10px 20px 10px;
+            width: 338px;
+            height: 208px;
+            border-radius: 8px;
+            background-color: #F6F6F6;
+        }
+
+        .project:hover {
+            background-color: #EFEFEF;
+        }
+
+        .project:hover > .project_name {
+            color: #0275d8;
+        }
+
+        .project_name {
+            font-size: 36px;
+            color: #292b2c;
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+        }
+
+        .project-nav-tabs {
+            font-size: smaller;
+        }
+
+        .project-nav-tabs > .active > a {
+            background: #379cfb !important;
+        }
+
+        .project-service-name {
+            font-size: 24px;
+            font-weight: bold;
+            line-height: 1.25;
+            margin-top: 24px;
+            margin-bottom: 16px;
+        }
+
+        .table-bordered-project-service > tbody > tr {
+            height: 70px;
+        }
     </style>
     </style>
 
 
 {% endblock head %}
 {% endblock head %}
@@ -92,6 +143,7 @@
     var resource_path = window.location.pathname;
     var resource_path = window.location.pathname;
     var cur_url = resource_path;
     var cur_url = resource_path;
     var show_real_password_for_renew = false;
     var show_real_password_for_renew = false;
+    var sheet_tag = window.location.hash;
 
 
     $(document).ready(function() {
     $(document).ready(function() {
         page_size = $('#page_size').val();
         page_size = $('#page_size').val();
@@ -283,6 +335,8 @@
             $('#batch_adjust_ability_form_button').prop('disabled', true);
             $('#batch_adjust_ability_form_button').prop('disabled', true);
             $('#batch_adjust_ability_modal .form-group').removeClass('has-success');
             $('#batch_adjust_ability_modal .form-group').removeClass('has-success');
         });
         });
+
+        nav_to_sheet_tag();
     });
     });
 
 
     function refresh() {
     function refresh() {
@@ -791,6 +845,18 @@
         }
         }
     }
     }
 
 
+    function change_display_password_for_row(me) {
+        if ($(me).hasClass("icon-elusive-eye")) {
+            $(me).removeClass("icon-elusive-eye").addClass("icon-elusive-eye-off");
+            $(me).parent().children(".real_password").toggle();
+            $(me).parent().children(".unreal_password").toggle();
+        } else {
+            $(me).removeClass("icon-elusive-eye-off").addClass("icon-elusive-eye");
+            $(me).parent().children(".unreal_password").toggle();
+            $(me).parent().children(".real_password").toggle();
+        }
+    }
+
     function change_display_password_for_renew(me) {
     function change_display_password_for_renew(me) {
         if (show_real_password_for_renew) {
         if (show_real_password_for_renew) {
             show_real_password_for_renew = false;
             show_real_password_for_renew = false;
@@ -802,14 +868,137 @@
             $('#new_password').attr({type: "text"});
             $('#new_password').attr({type: "text"});
         }
         }
     }
     }
+
+    function select_this(me) {
+        $(me).parent().parent().children().removeClass('active');
+        $(me).parent().addClass('active');
+
+        $('#view_by_original, #view_by_project').css('display', 'none');
+
+        if (me.id === 'view_by_project_label') {
+            $('#view_by_project').css('display', 'grid');
+            history.replaceState(null, null, window.location.pathname);
+            window.location.hash = "project";
+            refresh_project();
+        } else {
+            $('#view_by_original').css('display', 'unset');
+            history.replaceState(null, null, ' ');
+        }
+    }
+
+    function nav_to_sheet_tag() {
+        var view_by_original_label = $('#view_by_original_label');
+        var view_by_project_label = $('#view_by_project_label');
+
+        view_by_original_label.parent().parent().children().removeClass('active');
+
+        $('#view_by_original, #view_by_project').css('display', 'none');
+
+        if (sheet_tag.indexOf('#project') != -1) {
+            view_by_project_label.parent().addClass('active');
+            $('#view_by_project').css('display', 'grid');
+            refresh_project();
+        } else {
+            view_by_original_label.parent().addClass('active');
+            $('#view_by_original').css('display', 'unset');
+        }
+    }
+
+    function go_into_project(me) {
+        var project_id = $(me).data('project_id');
+        window.location.hash = ['project', project_id].join('#');
+        refresh_project();
+    }
+
+    function refresh_project_cards() {
+        $.ajax({
+            url : '/api/projects',
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            error : function() {
+            },
+            success : function(data, textStatus, xhr) {
+                $('#project_tool_bar').css('display', 'unset');
+                $('#project_cards').css('display', 'unset');
+                $('#project_cards').empty();
+                $.each(data.data, function(k, v) {
+                    $('#project_cards').append(
+                        '<a class="project" href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '"><span class="project_name">' + v['name'] + '</span></a>'
+                    );
+                });
+            }
+        });
+    }
+
+    function refresh_services(project_id) {
+        $('#project_services').css('display', 'unset');
+    }
+
+    function refresh_project() {
+        var project_id = window.location.hash.replace(/\#/, "").split('#');
+
+        $('#project_tool_bar').css('display', 'none');
+        $('#project_cards').css('display', 'none');
+        $('#project_tabs').css('display', 'none');
+        $('#project_desc').css('display', 'none');
+        $('#project_services').css('display', 'none');
+
+        if (project_id.length === 2) {
+            project_id = project_id[1];
+
+            $.ajax({
+                url : '/api/projects',
+                type : 'GET',
+                contentType: "application/json; charset=utf-8",
+                dataType: 'json',
+                error : function() {
+                },
+                success : function(data, textStatus, xhr) {
+                    $('#project_tabs').css('display', 'unset');
+                    $('#project_tabs').empty();
+                    $('#project_tabs').append('<ul class="nav nav-tabs project-nav-tabs mrg25B"></ul>');
+                    $.each(data.data, function(k, v) {
+                        if (v['id'].toString() === project_id) {
+                            $('#project_tabs > ul').append(
+                                '<li class="active"><a href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '">' + v['name'] + '</a></li>'
+                            );
+                        } else {
+                            $('#project_tabs > ul').append(
+                                '<li><a href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '">' + v['name'] + '</a></li>'
+                            );
+                        }
+                    });
+                }
+            });
+
+            $.ajax({
+                url : '/api/projects/' + project_id,
+                type : 'GET',
+                contentType: "application/json; charset=utf-8",
+                dataType: 'json',
+                error : function() {
+                },
+                success : function(data, textStatus, xhr) {
+                    $('#project_desc').css('display', 'unset');
+                    $('#project_desc').text(data.data['description']);
+                }
+            });
+
+            refresh_services(project_id);
+        } else {
+            refresh_project_cards();
+        }
+    }
 </script>
 </script>
 <div class="panel">
 <div class="panel">
     <div class="panel-body">
     <div class="panel-body">
-        <h3 class="title-hero" style="font-size: 24px;">
-            虚拟机实例
-        </h3>
+        <ul class="nav nav-tabs mrg25B">
+            <li class="active"><a href="javascript:;" onclick="select_this(this);" id="view_by_original_label">原始视图</a></li>
+            <li><a href="javascript:;" onclick="select_this(this);" id="view_by_project_label">项目视图</a></li>
+        </ul>
         <div>
         <div>
-            <div id="datatable-row-highlight_wrapper" class="dataTables_wrapper form-inline">
+            <div id="view_by_original" class="dataTables_wrapper form-inline">
                 <div class="row" style="padding: 10px 10px 10px 0; width: 100%;">
                 <div class="row" style="padding: 10px 10px 10px 0; width: 100%;">
                     <div class="col-sm-12" style="padding-right: 0;">
                     <div class="col-sm-12" style="padding-right: 0;">
                         <div id="datatable-row-highlight_filter" class="dataTables_filter" style="display: inline-block;">
                         <div id="datatable-row-highlight_filter" class="dataTables_filter" style="display: inline-block;">
@@ -1087,6 +1276,193 @@
                     </tfoot>
                     </tfoot>
                 </table>
                 </table>
             </div>
             </div>
+            <div id="view_by_project" class="dataTables_wrapper form-inline" style="display: none;">
+                <div id="project_tool_bar" class="row" style="padding: 10px 10px 10px 0; width: 100%; display: none;">
+                    <div class="col-sm-12" style="padding-right: 0;">
+                        <div class="pull-right">
+                            <a class="btn btn-info add-page-transition" href="/guests/create" data-transition="pt-page-moveFromRight-init" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">新建项目</a>
+                        </div>
+                    </div>
+                </div>
+                <div style="margin-top: 20px;">
+                    <div id="project_tabs" style="display: none;"></div>
+                    <div id="project_desc" style="display: none;"></div>
+                    <div id="project_services" style="display: none;">
+                        <div id="project_service_name" class="project-service-name">Web</div>
+                        <div id="project_service_guests">
+                            <table id="guest_list" class="table-hover table-bordered-project-service" cellspacing="0" width="100%" role="grid"
+                                   style="width: 100%; margin-bottom: 4px; border-bottom-width: 0;">
+                                <tbody>
+                                {% for item in guests %}
+                                    <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                                        <td style="display: none;">{{ item.uuid }}</td>
+                                        <td><input title="选中" type="checkbox"></td>
+                                        <td width="180px;">
+                                            <div>
+                                                <a href="/guest/detail/{{ item.uuid }}">{{ item.label }}</a>
+                                            </div>
+                                            <div>
+                                                <p style="display: inline-block;">{{ item.remark }}</p>
+                                                <a href="javascript:;" class="edit_remark_trigger" data-toggle="modal" data-target="#edit_remark_modal" style="display: none; float: right;">
+                                                    <span class="glyph-icon icon-elusive-pencil" style="width: 20px; height: 20px; margin-left: 10px; border-radius: 0; border: 1px solid rgb(220, 233, 255); background-color: #ffffff;"></span>
+                                                </a>
+                                            </div>
+                                        </td>
+                                        <td width="40px" style="padding-left: 12px;">
+                                            {% if os_templates_image_mapping_by_id[item.os_template_image_id | string].logo == "" %}
+                                                <span class="{{ os_templates_profile_mapping_by_id[os_templates_image_mapping_by_id[item.os_template_image_id | string].os_template_profile_id].icon }}" title="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].label }}"></span>
+                                            {% else %}
+                                                <span class="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].logo }}" title="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].label }}"></span>
+                                            {% endif %}
+                                        </td>
+                                        <td>{{ format_guest_status(item.status, item.progress)|safe }}</td>
+                                        <td>{{ hosts_mapping_by_node_id[item.node_id | string].hostname }}</td>
+                                        <td>
+                                            CPU :&nbsp;&nbsp;{{ item.cpu }}&nbsp;核<br />
+                                            内存 :&nbsp;&nbsp;{{ item.memory }}&nbsp;GB<br />
+                                            带宽 :&nbsp;
+                                            {% if item.bandwidth == 0 %}
+                                                <span style="font-size: 16px;" title="无限带宽">
+                                &nbsp;∞
+                                </span>
+                                            {% elif item.bandwidth > 0 and item.bandwidth < 1000**2 %}
+                                                {{ item.bandwidth // 1000 }}
+
+                                            {% elif item.bandwidth >= 1000**2 and item.bandwidth < 1000**3 %}
+                                                {{ item.bandwidth // 1000**2 }}
+
+                                            {% else %}
+                                                {{ item.bandwidth // 1000**3 }}
+
+                                            {% endif %}
+
+                                            {% if item.bandwidth == 0 %}
+                                            {% elif item.bandwidth > 0 and item.bandwidth < 1000**2 %}
+                                                Kbps
+                                            {% elif item.bandwidth >= 1000**2 and item.bandwidth < 1000**3 %}
+                                                Mbps
+                                            {% else %}
+                                                Gbps
+                                            {% endif %}
+                                        </td>
+                                        <td>{{ item.ip }}</td>
+                                        <td width="150px;"><a href="#" class="glyph-icon icon-elusive-eye-off" onclick="change_display_password_for_row(this);" style="color: #014c8c;"></a><span class="unreal_password">******</span><span class="real_password" style="display: none;">{{ item.password }}</span></td>
+                                        <td>
+                                            <div class="dropdown inline-block">
+                                                <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
+                                                    更多
+                                                </a>
+                                                <ul class="dropdown-menu">
+                                                    <li class="{% if item.status not in [5, 6, 7] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#boot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#boot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            启动
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#reboot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#reboot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            重启
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#force_reboot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#force_reboot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            强制重启
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#shutdown_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#shutdown_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            停止
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#force_shutdown_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#force_shutdown_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            强制停止
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if not item.snapshot.creatable %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#create_snapshot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#create_snapshot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent);
+                                                $('#snapshot_label').val($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            创建快照
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#suspend_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#suspend_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            暂停
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [4] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#resume_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#resume_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            恢复
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [1, 2] %} disabled {% endif %}">
+                                                        <a href="/guest/vnc/{{ item.uuid }}" target="_blank">远程连接</a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#allocate_bandwidth_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#allocate_bandwidth_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            分配带宽
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [6] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#adjust_ability_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#adjust_ability_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            变更配置
+                                                        </a>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#reset_password_modal"
+                                                           onclick="$('#reset_password_type').val('single')">
+                                                            密码重置
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2, 4, 5, 6, 7] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#migrate_modal"
+                                                           onclick="$('#migrate_type').val('single')">
+                                                            迁移到
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [5, 6, 7, 255] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#delete_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            删除
+                                                        </a>
+                                                    </li>
+                                                </ul>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                    <div id="project_cards" style="display: none;"></div>
+                </div>
+            </div>
         </div>
         </div>
     </div>
     </div>
 </div>
 </div>

+ 32 - 0
misc/init.sql

@@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS guest(
     status TINYINT UNSIGNED NOT NULL DEFAULT 0,
     status TINYINT UNSIGNED NOT NULL DEFAULT 0,
     progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
     progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
     node_id BIGINT UNSIGNED NOT NULL,
     node_id BIGINT UNSIGNED NOT NULL,
+    service_id BIGINT UNSIGNED NOT NULL default 1,
     cpu TINYINT UNSIGNED NOT NULL,
     cpu TINYINT UNSIGNED NOT NULL,
     memory INT UNSIGNED NOT NULL,
     memory INT UNSIGNED NOT NULL,
     -- bps
     -- bps
@@ -56,6 +57,7 @@ CREATE TABLE IF NOT EXISTS guest(
 ALTER TABLE guest ADD INDEX (uuid);
 ALTER TABLE guest ADD INDEX (uuid);
 ALTER TABLE guest ADD INDEX (label);
 ALTER TABLE guest ADD INDEX (label);
 ALTER TABLE guest ADD INDEX (node_id);
 ALTER TABLE guest ADD INDEX (node_id);
+ALTER TABLE guest ADD INDEX (service_id);
 ALTER TABLE guest ADD INDEX (ip);
 ALTER TABLE guest ADD INDEX (ip);
 ALTER TABLE guest ADD INDEX (remark);
 ALTER TABLE guest ADD INDEX (remark);
 
 
@@ -420,6 +422,36 @@ ALTER TABLE snapshot_disk_mapping ADD UNIQUE INDEX (snapshot_id, disk_uuid);
 ALTER TABLE snapshot_disk_mapping ADD INDEX (disk_uuid);
 ALTER TABLE snapshot_disk_mapping ADD INDEX (disk_uuid);
 
 
 
 
+CREATE TABLE IF NOT EXISTS project(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE project ADD INDEX (name);
+
+
+CREATE TABLE IF NOT EXISTS service(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    project_id BIGINT UNSIGNED NOT NULL,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE service ADD INDEX (project_id);
+ALTER TABLE service ADD INDEX (name);
+
+
+INSERT INTO project (name, description, create_time) VALUES ('我的项目', '由 JimV 创建的默认项目。', UNIX_TIMESTAMP(NOW()) * 1000000);
+INSERT INTO service (project_id, name, description, create_time) VALUES (1, '服务组', '由 JimV 创建的默认服务组。', UNIX_TIMESTAMP(NOW()) * 1000000);
+
+
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-Systemd', '用作 Redhat Systemd 系列的系统初始化。初始化操作依据 CentOS 7 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-Systemd', '用作 Redhat Systemd 系列的系统初始化。初始化操作依据 CentOS 7 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-SysV', '用作 Redhat SysV 系列的系统初始化。初始化操作依据 CentOS 6.8 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-SysV', '用作 Redhat SysV 系列的系统初始化。初始化操作依据 CentOS 6.8 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('Gentoo-OpenRC', '用作 Gentoo OpenRC 系列的系统初始化。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('Gentoo-OpenRC', '用作 Gentoo OpenRC 系列的系统初始化。', 1);

+ 34 - 0
misc/v0.6_to_v0.7/update.sql

@@ -0,0 +1,34 @@
+
+CREATE TABLE IF NOT EXISTS project(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE project ADD INDEX (name);
+
+
+CREATE TABLE IF NOT EXISTS service(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    project_id BIGINT UNSIGNED NOT NULL,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE service ADD INDEX (project_id);
+ALTER TABLE service ADD INDEX (name);
+
+
+INSERT INTO project (name, description, create_time) VALUES ('我的项目', '由 JimV 创建的默认项目。', UNIX_TIMESTAMP(NOW()) * 1000000);
+INSERT INTO service (project_id, name, description, create_time) VALUES (1, '服务组', '由 JimV 创建的默认服务组。', UNIX_TIMESTAMP(NOW()) * 1000000);
+
+
+ALTER TABLE guest ADD COLUMN service_id BIGINT UNSIGNED NOT NULL DEFAULT 1;
+ALTER TABLE guest ADD INDEX (service_id);
+