diff --git a/Aries/Aries/settings.py b/Aries/Aries/settings.py index 5cca9f4..378b12f 100644 --- a/Aries/Aries/settings.py +++ b/Aries/Aries/settings.py @@ -34,7 +34,7 @@ yaml_file = open(file_name) OPENSTACK_KEY_PATH = os.path.join(BASE_DIR,"openstack/middleware/common/key.yaml") SETTINGS = yaml.load(yaml_file) -print SETTINGS +# print SETTINGS # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ @@ -152,8 +152,14 @@ 'level':'DEBUG', 'class':'logging.FileHandler', 'formatter': 'complete', - 'filename' :'{0}/service.log'.format(LOG_BASE_DIR).replace('\\','/') + 'filename' :'{0}/kd_agent.log'.format(LOG_BASE_DIR).replace('\\','/') }, + 'kd_agent_pushclusterinfo_file': { + 'level':'DEBUG', + 'class':'logging.FileHandler', + 'formatter': 'complete', + 'filename' :'{0}/kd_agent_pushclusterinfo.log'.format(LOG_BASE_DIR).replace('\\','/') + }, 'openstack_log': { 'level': 'DEBUG', 'class': 'logging.FileHandler', @@ -194,6 +200,11 @@ 'kd_agent_log': { 'handlers':['kd_agent_file','console'], 'propagate': False, + 'level':'INFO', + }, + 'kd_agent_pushclusterinfo_log': { + 'handlers':['kd_agent_pushclusterinfo_file','console'], + 'propagate': False, 'level':'DEBUG', }, 'openstack_log': { @@ -310,10 +321,7 @@ PORT_CINDER = OPENSTACK_SETTINGS["PORT_CINDER"] MONITOR_URL = OPENSTACK_SETTINGS['MONITOR_URL'] -#启动一个线程开始定时统计配额. default: 10m -POLL_TIME = 600 -import sumSpace -sumSpace.run(POLL_TIME) + #admin页面白名单IP WHITELIST_SETTINGS = SETTINGS['WHITELIST'] diff --git a/Aries/hdfs/management/__init__.py b/Aries/hdfs/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Aries/hdfs/management/commands/__init__.py b/Aries/hdfs/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Aries/hdfs/management/commands/calcsumspace.py b/Aries/hdfs/management/commands/calcsumspace.py new file mode 100644 index 0000000..d090564 --- /dev/null +++ b/Aries/hdfs/management/commands/calcsumspace.py @@ -0,0 +1,19 @@ +# -*- coding: UTF-8 -*- + +from django.core.management.base import BaseCommand + +from Aries import sumSpace + +POLL_TIME = 600 + +#启动一个线程开始定时统计配额. default: 10m + +class Command(BaseCommand): + help = 'start a thread to calc statistical quota every 10 minutes' + + # 由于该脚本执行的命令较为简单,因此不接受参数 + def add_arguments(self, parser): + return None + + def handle(self, *args, **options): + sumSpace.run( POLL_TIME ) diff --git a/Aries/kd_agent/management/__init__.py b/Aries/kd_agent/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Aries/kd_agent/management/commands/__init__.py b/Aries/kd_agent/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Aries/kd_agent/management/commands/pushk8sdata.py b/Aries/kd_agent/management/commands/pushk8sdata.py new file mode 100644 index 0000000..89751df --- /dev/null +++ b/Aries/kd_agent/management/commands/pushk8sdata.py @@ -0,0 +1,119 @@ +# -*- coding: UTF-8 -*- + +import traceback +import datetime +import logging +import os +import requests +import json + +from django.core.management.base import BaseCommand, CommandError +from django.db import IntegrityError + +from kd_agent.toolsmanager import RETU_INFO_SUCCESS,RETU_INFO_ERROR +from kd_agent.toolsmanager import generate_success,generate_failure +from kd_agent.models import ResourceUsageDailyCache as RUDC +from kd_agent.models import NamespaceDepartmentRef as NDR +from kd_agent.models import ClusterUsagePushFailureRecords as CUPFR +from kd_agent.views import get_resource_usage_info + + +logger = logging.getLogger("kd_agent_pushclusterinfo_log") + + +# 将需要推送的数据的关键信息放到失败记录表中(ClusterUsagePushFailureRecords),之后再统一推送 +def refresh_failure_record(): + date = datetime.datetime.combine( datetime.datetime.now(),datetime.time() ) + yesterday = date - datetime.timedelta(seconds=24*60*60) + + # 在数据库中插入记录,然后统一根据记录来往运维推送 + for record in NDR.objects.all(): + try: + CUPFR( namespace=record,datetime=yesterday ).save() + logger.debug( 'insert undo record(%s,%s) success' % (record.namespace,yesterday) ) + except IntegrityError: # (主键重复的异常)如果数据库中已经存在了这条记录,则该异常可以直接忽略 + pass + except: + logger.error( 'insert undo record(%s,%s) failure : %s' % (record.namespace,yesterday,traceback.format_exc()) ) + +def get_push_url(): + return 'http://li.app/v1/om/source/Sirius/addSirius' + +def push_data(): + for record in CUPFR.objects.all(): + try: + namespace = record.namespace.namespace + department = record.namespace.department + + # django存到mysql的datetime对象是不带有时区的, + # # 因此这里为了方便处理,直接把不含时区的datetime对象转换为含有时区(本地时区)的datetime对象 + date = record.datetime+datetime.timedelta(seconds=8*60*60) + date = datetime.datetime.strptime( date.strftime('%Y-%m-%d'),'%Y-%m-%d' ) + + retu_data = push_identify_data(date,namespace,department) + if retu_data['code'] == RETU_INFO_SUCCESS: + record.delete() + logger.debug('push_identify_data(%s,%s,%s) success' % (date,namespace,department)) + else: + logger.error('push_identify_data(%s,%s,%s) failure : %s' % (date,namespace,department,retu_data['msg'])) + except: + logger.error( 'push_identify_data(%s,%s,%s) raise exception : %s' % (date,namespace,department,traceback.format_exc()) ) + +def push_identify_data(date,namespace,department): + retu_data = get_resource_usage_info( date,namespace ) + if retu_data['code'] != RETU_INFO_SUCCESS: + s = 'get_resource_usage_info(%s,%s) failure : %s' % (date,namespace,retu_data['msg']) + return generate_failure( s ) + return push_http(date,department,retu_data['data']['request']) + +''' +接口所接受的post数据的格式: + usage:[{ + department:'基础研发部' 标识部门名称的字符串,可能是二级部门、一级部门、中心的名字 + date:'2016-12-01' 标识该记录是哪个时间段的数据统计汇总出来的( 2016-12-01 00:00:00:000 至 2016-12-01 59:59:59:999 ) + usage:'11.03' 标识该部门在这天的机器用量,单位是 机器/天 + },{ + ... + }] + +备注:接口支持一次性传输多条记录,但是我这里为了方便,每次只传输一条记录 +''' +def push_http(date,department,usage): + post_data = { + 'usage':[{ + 'department':department, + 'date':date.strftime('%Y-%m-%d'), + 'usage':str(usage) + }] + } + req = requests.post(get_push_url(), data=json.dumps(post_data)) + if req.status_code != requests.codes.ok: + s = 'requests.post(%s,%s) return req.status_code is not requests.codes.ok' % \ + ( get_push_url(),json.dumps(post_data) ) + return generate_failure( s ) + + retu_obj = req.json() + if retu_obj['status'] == True: + return generate_success() + else: + s = 'requests.post(%s,%s) return status is not True : %s' % ( get_push_url(),json.dumps(post_data),retu_obj ) + return generate_failure( s ) + + +class Command(BaseCommand): + help = 'Push k8s cluster usage to operation' + + # 由于该脚本执行的命令较为简单,因此不接受参数 + def add_arguments(self, parser): + return None + + def handle(self, *args, **options): + command_str = str(__file__) + command_str = os.path.split(command_str)[1] + command_str = os.path.splitext(command_str)[0] + try: + refresh_failure_record() + push_data() + logger.info( 'execute command %s success' % command_str ) + except: + logger.error( 'execute command %s failure : \n%s' % (command_str,traceback.format_exc()) ) diff --git a/Aries/kd_agent/models.py b/Aries/kd_agent/models.py index 5795eeb..bd09244 100644 --- a/Aries/kd_agent/models.py +++ b/Aries/kd_agent/models.py @@ -1 +1,117 @@ # -*- coding: UTF-8 -*- + +from django.db import models +import math + + +from kd_agent.toolsmanager import InfluxDBQueryStrManager as ISM + +def calc_minute_ave(v): + return float(v)/(24*60) + +# 建库语句 +# create database DecisionMakingSurvey default charset utf8 collate utf8_unicode_ci; + +class ResourceUsageDailyCache(models.Model): + datetime = models.DateTimeField() + namespace = models.CharField( max_length=255 ) + + cpu_request = models.BigIntegerField( default=0 ) + cpu_limit = models.BigIntegerField( default=0 ) + cpu_usage = models.BigIntegerField( default=0 ) + + # 这里的memory的workingset指标没有被保存。如果有需要,这里再保存一下即可 + memory_request = models.BigIntegerField( default=0 ) + memory_limit = models.BigIntegerField( default=0 ) + memory_usage = models.BigIntegerField( default=0 ) + + # 注意,上面的 cpu、memory 指标数据是相对比较原始的数据,即将 1 天内 1440 个分钟的采样数据全部加起来 + # 但是下面的 request、limit、usage 数据是经过计算之后得到的(calc_virtual_machine_day) + + request = models.FloatField() + limit = models.FloatField() + usage = models.FloatField() + + # 方便地生成一个对象 + # data_json 是一个json对象,其中的key应该与 ISM 中定义的一致,如下 + # ISM.M_CPU_USAGE + # ISM.M_CPU_LIMIT + # ISM.M_CPU_REQUEST + # ISM.M_MEMORY_USAGE + # ISM.M_MEMORY_LIMIT + # ISM.M_MEMORY_REQUEST + @staticmethod + def generate_obj_by_measurement_key( datetime,namespace,data_json ): + keys_map = { + ISM.M_CPU_REQUEST:'cpu_request', + ISM.M_CPU_LIMIT:'cpu_limit', + ISM.M_CPU_USAGE:'cpu_usage', + ISM.M_MEMORY_REQUEST:'memory_request', + ISM.M_MEMORY_LIMIT:'memory_limit', + ISM.M_MEMORY_USAGE:'memory_usage', + } + obj = {} + for k,v in keys_map.items(): + obj[ v ] = data_json.get(k,0) + return ResourceUsageDailyCache.generate_obj_by_base_keys( datetime,namespace,obj ) + + @staticmethod + def generate_obj_by_base_keys( datetime,namespace,obj ): + CVMD = ResourceUsageDailyCache.calc_virtual_machine_day + # 根据提供的cpu、memory、usage数据来计算资源用量 + obj['usage'] = CVMD( obj['cpu_usage'],obj['memory_usage'] ) + obj['limit'] = CVMD( obj['cpu_limit'],obj['memory_limit'] ) + obj['request'] = CVMD( obj['cpu_request'],obj['memory_request'] ) + return ResourceUsageDailyCache( datetime=datetime,namespace=namespace,**obj ) + + def to_minuteaverge_measurementkey_json(self): + return { + ISM.M_CPU_REQUEST:calc_minute_ave(self.cpu_request), + ISM.M_CPU_LIMIT:calc_minute_ave(self.cpu_limit), + ISM.M_CPU_USAGE:calc_minute_ave(self.cpu_usage), + ISM.M_MEMORY_REQUEST:calc_minute_ave(self.memory_request), + ISM.M_MEMORY_LIMIT:calc_minute_ave(self.memory_limit), + ISM.M_MEMORY_USAGE:calc_minute_ave(self.memory_usage), + 'request':self.request, + 'limit':self.limit, + 'usage':self.usage, + } + + # u 多少个0.5VCPU (1VCPU == cpu/1000 ,0.5VCPU是预设的值) + # v 多少个128MB内存 ( 128MB是预设的值 ) + # 计算公式为: u*0.025 + 0.003*v / (8*u) + # 结果保留11位有效小数(1e-11)(且直接进位) 即如果结果是 1.11e-11 则,应该显示 1.2e-11 + @staticmethod + def calc_virtual_machine_day( cpu_value,memory_value ): + + u = calc_minute_ave(cpu_value)/1000/0.5 + v = calc_minute_ave(memory_value)/128/1024/1024 + try: + value = u*0.025 + 0.003*v / (8*u) + except: + value = 0 + + # 先放大1e11倍,然后向上取整。之后再缩小1e11倍。由于缩小之后,获取的数不会严格1e11倍,因此round一下 + # 如: + # >>> 1.3/10000 + # 0.00013000000000000002 + d = 1e-11 + return round( math.ceil(value/d)*d,11 ) + + +# 该表中记录namespace到部门名的一个对照,只用做向运维推送集群每天某个namespace的资源用量 +# 由于推送数据与Sirius的主要业务不相关,因此这里会尽量降低它与Sirius业务相关表的耦合度 +class NamespaceDepartmentRef(models.Model): + namespace = models.CharField( max_length=255,primary_key=True ) + department = models.CharField( max_length=1024 ) + +# 该表是为了记录推送的状态。比如某天的数据推送失败,则该表中将会有一条push_success为False的记录 +# 等到下次推送的时候,将会查找这些记录并重新推送 +class ClusterUsagePushFailureRecords(models.Model): + namespace = models.ForeignKey( NamespaceDepartmentRef ) + datetime = models.DateTimeField() + class Meta: + unique_together = ("namespace", "datetime") + + + diff --git a/Aries/kd_agent/toolsmanager.py b/Aries/kd_agent/toolsmanager.py index bf8670f..380c4bd 100644 --- a/Aries/kd_agent/toolsmanager.py +++ b/Aries/kd_agent/toolsmanager.py @@ -127,6 +127,12 @@ class InfluxDBQueryStrManager: WHERE "type" = '{type}' AND "pod_namespace" = '{namespace}' AND "pod_name"='{pod_name}' AND time > {time_start} and time < {time_end} GROUP BY time(1m) fill(null)''' + # 这里本来应该直接 group by time(1440m) (汇总1天数据),但是不知道为什么汇总出来的是2条数据 + # 因此就降一级,按照小时汇总数据,然后吧所有数据都加起来 + SQL_NAMESPACE_RESOURCE_USAGE = '''SELECT sum("value") FROM "{measurement}" + WHERE "type" = '{type}' AND "pod_namespace" = '{namespace}' AND time >= {time_start} and time < {time_end} + GROUP BY time(60m) fill(null)''' + M_CPU_USAGE = 'cpu/usage_rate' M_CPU_LIMIT = 'cpu/limit' @@ -169,6 +175,16 @@ def format_namespace_poddetail_query_str(measurement,time_start ,time_end ,names type=InfluxDBQueryStrManager.T_POD, pod_name=pod_name) + @staticmethod + def format_namespace_resourceusage_query_str(measurement,time_start ,time_end ,namespace ): + return InfluxDBQueryStrManager.SQL_NAMESPACE_RESOURCE_USAGE.format( + measurement=measurement, + time_start=time_start, + time_end=time_end, + namespace=namespace, + type=InfluxDBQueryStrManager.T_POD) + + @staticmethod def get_measurement_disname_dict(): @@ -270,6 +286,27 @@ def get_namespace_poddetail_data( measurement,time_start,time_end,namespace,pod_ kd_logger.error( traceback_str ) return generate_failure( traceback_str ) + @staticmethod + def get_namespace_resourceusage_data( measurement,time_start,time_end,namespace ): + kd_logger.info( 'call get_namespace_resourceusage_data with args : %s %s %s %s' % (measurement,time_start,time_end,namespace) ) + try: + sql_str = InfluxDBQueryStrManager.format_namespace_resourceusage_query_str( + measurement=measurement, + time_start='%ss' % time_start , + time_end='%ss' % time_end, + namespace=namespace) + kd_logger.info( 'generate sql_str : %s' % (sql_str) ) + + retu_data = InfluxDBQueryStrManager.get_influxdb_data(sql_str=sql_str) + if retu_data['code'] == RETU_INFO_SUCCESS: + kd_logger.debug( 'get influxdb data by sql_str return data : %s' % retu_data['data'] ) + else: + kd_logger.error( 'get influxdb data by sql_str return error : %s' % retu_data['msg'] ) + return retu_data + except: + traceback_str = traceback.format_exc() + kd_logger.error( traceback_str ) + return generate_failure( traceback_str ) diff --git a/Aries/kd_agent/urls.py b/Aries/kd_agent/urls.py index 53b853b..f395f33 100644 --- a/Aries/kd_agent/urls.py +++ b/Aries/kd_agent/urls.py @@ -15,6 +15,8 @@ url(r'^api/namespaces/(?P\w{1,64})/replicationcontrollers/downloadjson$',views.download_rc_json), url(r'^apis/extensions/v1beta1/namespaces/(?P\w{1,64})/ingresses/downloadjson$',views.download_ingress_json ), + url(r'^api/namespaces/(?P\w{1,64})/resourceusage$',views.resource_usage), + url(r'^api/namespaces/mytaskgraph$', views.get_mytask_graph), url(r'^download/$', views.download), url(r'^api/namespaces/mytasklist/getoldrecords', views.mytask_get_old_records), diff --git a/Aries/kd_agent/views.py b/Aries/kd_agent/views.py index 4cc401b..9b41f25 100644 --- a/Aries/kd_agent/views.py +++ b/Aries/kd_agent/views.py @@ -8,7 +8,11 @@ import codecs from django.http import StreamingHttpResponse -from datetime import datetime +from datetime import datetime,timedelta + +# 由于前面有 import time ,为了防止重名,因此这里改下名 +from datetime import time as datetime_time + from django.conf import settings from django.views.decorators.csrf import csrf_exempt from django.db.models import Q @@ -21,7 +25,7 @@ from kd_agent.toolsmanager import K8sRequestManager as KRM from kd_agent.toolsmanager import InfluxDBQueryStrManager as ISM - +from kd_agent.models import ResourceUsageDailyCache as RUDC kd_logger = logging.getLogger("kd_agent_log") @@ -704,6 +708,101 @@ def get_poddetail_filesystem_info(request,namespace,minutes): return execute_clusterinfo_request( data_dict,time_range ) +# 这里获取某个时间段内,cpu、memory相应指标的总和 +def get_resource_usage_from_influxdb( start_date,end_date,namespace ): + # 由于传入的时间是local time,而influxdb中保存的时间戳是 utc time,因此需要做一个简单的转换 + s_timestamp = int(time.mktime( start_date.timetuple() )) + e_timestamp = int(time.mktime( end_date.timetuple() )) + + # 这里只计算CPU、Memory的 + measurements = [ ISM.M_CPU_USAGE,ISM.M_CPU_LIMIT,ISM.M_CPU_REQUEST ] + \ + [ ISM.M_MEMORY_USAGE,ISM.M_MEMORY_LIMIT,ISM.M_MEMORY_REQUEST ] + + retu_obj = {} + for m in measurements: + retu_data = ISM.get_namespace_resourceusage_data( m,s_timestamp,e_timestamp,namespace ) + + if retu_data['code'] != RETU_INFO_SUCCESS: + return retu_data + + # 如果某个space下,influxdb返回的 results 为是空数组,则表明数据库中没有该筛选条件下的数据,因此置为0 + try: + data_arr = [ item[1] if item[1] else 0 \ + for item in retu_data['data']['results'][0]['series'][0]['values'] ] + retu_obj[m] = sum(data_arr) + except Exception as e: + retu_obj[m] = 0 + + return generate_success( data = retu_obj ) + + +# 检查mysql数据库中是否已经缓存了数据。如果有,则直接返回;如果没有,则现查 +# 注意,如果 start_date 的年月日与今天的年月日相同,则直接查influxdb,而不存mysql缓存 +def get_resource_usage_info( start_date,namespace ): + # 保证 start_date 时分秒都为0 + start_date = datetime.combine( start_date,datetime_time() ) + end_date = start_date+timedelta(seconds=24*60*60) + + # 如果 start_date 与当前的年月日相同或者大,则直接查询influxdb并返回,且不缓存数据到mysql + if start_date >= datetime.combine( datetime.now(),datetime_time() ): + retu_data = get_resource_usage_from_influxdb( start_date,end_date,namespace ) + if retu_data['code'] != RETU_INFO_SUCCESS: + return retu_data + else: + obj = RUDC.generate_obj_by_measurement_key( start_date,namespace,retu_data['data'] ) + return generate_success( data=obj.to_minuteaverge_measurementkey_json() ) + else: + records = list(RUDC.objects.filter( datetime=start_date,namespace=namespace )) + if len(records) == 0: + # 如果数据库中无缓存,则需要查询influxdb + retu_data = get_resource_usage_from_influxdb( start_date,end_date,namespace ) + + # 查询失败,则直接返回 + if retu_data['code'] != RETU_INFO_SUCCESS: + return retu_data + + # 如果查询成功,但是缓存失败,也可以直接返回,而不做任何处理 + obj = RUDC.generate_obj_by_measurement_key( start_date,namespace,retu_data['data'] ) + try: + obj.save() + except: + pass + return generate_success(data=obj.to_minuteaverge_measurementkey_json()) + else: + return generate_success(data=records[0].to_minuteaverge_measurementkey_json()) + + +@csrf_exempt +@return_http_json +@trans_return_json +def resource_usage(request,namespace): + start_date_str = request.GET.get('startdate',datetime.now().strftime('%Y-%m-%d')) + days = int(request.GET.get('days',1)) + + # 从时间字符串初始化一个datetime对象 + start_date = datetime.strptime( start_date_str,'%Y-%m-%d' ) + + retu_infos = [] + for index in range(days): + s = start_date+timedelta(seconds=24*60*60)*index + retu_data = get_resource_usage_info(s,namespace) + + if retu_data['code'] != RETU_INFO_SUCCESS: + return retu_data + + # date 是可以直接显示到页面上的日期(没有时分秒) + retu_infos.append({ + 'date':s.strftime('%Y-%m-%d'), + 'data':retu_data['data'] + }) + + return generate_success(data=retu_infos) + + + + + + diff --git a/Dockerfile b/Dockerfile index 1d572bc..3f3826b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM docker.baifendian.com/sjx/sirius_base -MAINTAINER jingxia.sun +FROM docker.baifendian.com/guopan/sirius_base +MAINTAINER pan.guo ENV SIRIUS_PATH /opt/Sirius RUN mkdir -p /opt/Sirius @@ -39,6 +39,3 @@ RUN chmod +x $SIRIUS_PATH/sbin/Aries.sh &&\ EXPOSE 10086 #CMD /opt/Sirius/sbin/Aries.sh start >>/opt/Sirius/log/uwsgi.log CMD sh $SIRIUS_PATH/docker-k8s/script/start_script.sh - - - diff --git a/docker-k8s/script/start_script.sh b/docker-k8s/script/start_script.sh index 6dc6088..716a7ad 100644 --- a/docker-k8s/script/start_script.sh +++ b/docker-k8s/script/start_script.sh @@ -95,5 +95,15 @@ sed -i "s#YARN_YARN_RESOURCEMANAGER_WEBAPP_ADDRESS_RM2#$YARN_YARN_RESOURCEMANAGE sed -i "s#YARN_YARN_RESOURCEMANAGER_RESOURCE_TRACKER_ADDRESS_RM2#$YARN_YARN_RESOURCEMANAGER_RESOURCE_TRACKER_ADDRESS_RM2#g" /opt/hadoop/etc/hadoop/yarn-site.xml sed -i "s#YARN_YARN_RESOURCEMANAGER_ADMIN_ADDRESS_RM2#$YARN_YARN_RESOURCEMANAGER_ADMIN_ADDRESS_RM2#g" /opt/hadoop/etc/hadoop/yarn-site.xml + +# 启动一个crontab进程,定时往运维推送数据 +sed -i '/session required pam_loginuid.so/c\session sufficient pam_loginuid.so' /etc/pam.d/crond +echo '0 2 * * * /opt/Python-2.7/bin/python /opt/Sirius/Aries/manage.py pushk8sdata' >> /var/spool/cron/root +chmod 600 /var/spool/cron/root +service crond restart + +nohup /opt/Python-2.7/bin/python /opt/Sirius/Aries/manage.py calcsumspace > /opt/Sirius/log/nohup.out 2>&1 & + source /etc/profile /opt/Sirius/sbin/Aries.sh start + diff --git a/package/Aries/package.json b/package/Aries/package.json index 1579d25..3f980b6 100644 --- a/package/Aries/package.json +++ b/package/Aries/package.json @@ -8,7 +8,7 @@ "dependencies": { "antd": "^1.8.0", "bfd-bootstrap": "0.0.22", - "bfd-ui": "^1.3.0", + "bfd-ui": "^1.5.0", "echarts": "^3.2.3", "js-cookie": "^2.1.2", "less": "^2.7.1", diff --git a/package/Aries/src/App.jsx b/package/Aries/src/App.jsx index 94de6f2..61234d3 100644 --- a/package/Aries/src/App.jsx +++ b/package/Aries/src/App.jsx @@ -162,6 +162,8 @@ const App = React.createClass({ + + {/* 暂时下面没有任何节点,因此注释掉 diff --git a/package/Aries/src/functions/CalcManage/CalcManageDataRequester/requester.js b/package/Aries/src/functions/CalcManage/CalcManageDataRequester/requester.js index de04793..b7bf0d2 100644 --- a/package/Aries/src/functions/CalcManage/CalcManageDataRequester/requester.js +++ b/package/Aries/src/functions/CalcManage/CalcManageDataRequester/requester.js @@ -37,9 +37,19 @@ var CalcManageDataRequester = { 'rcjsondownload': '{baseUrl}v1/k8s/api/namespaces/{nameSpace}/replicationcontrollers/downloadjson?rcname={rcName}', 'ingressjsondownload': '{baseUrl}v1/k8s/apis/extensions/v1beta1/namespaces/{nameSpace}/ingresses/downloadjson?ingressname={ingressName}', + 'resourceusage': 'v1/k8s/api/namespaces/{nameSpace}/resourceusage?startdate={startdate}&days={days}' } }, + getResourceUsageInfo( nameSpace, startdate, days, callback){ + let url = Toolkit.strFormatter.formatString(this.getUrlForm()['resourceusage'],{ + 'nameSpace':nameSpace, + 'startdate': startdate, + 'days': days + }) + this.xhrGetDataEnhanced(url, callback) + }, + // 下载文件必须使用 button + url 的方式,不能使用ajax请求,因此这里只返回 url getPodJsonDownloadUrl(nameSpace, podName){ return Toolkit.strFormatter.formatString(this.getUrlForm()['podjsondownload'], { diff --git a/package/Aries/src/functions/CalcManage/ClusterInfo/poddetail.jsx b/package/Aries/src/functions/CalcManage/ClusterInfo/poddetail.jsx index 7e3485e..11596ba 100644 --- a/package/Aries/src/functions/CalcManage/ClusterInfo/poddetail.jsx +++ b/package/Aries/src/functions/CalcManage/ClusterInfo/poddetail.jsx @@ -220,7 +220,7 @@ var PodDetailElement = React.createClass({ let userUnSelectedPodDetailInfo = [['请选择Pod']] return( - + 容器基本信息 监控 diff --git a/package/Aries/src/functions/CalcManage/ResourceUsageBilling/index.jsx b/package/Aries/src/functions/CalcManage/ResourceUsageBilling/index.jsx new file mode 100644 index 0000000..0424842 --- /dev/null +++ b/package/Aries/src/functions/CalcManage/ResourceUsageBilling/index.jsx @@ -0,0 +1,256 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import FixedTable from 'bfd/FixedTable' +import message from 'bfd/message' + +import MonthSelectControl from 'public/MonthSelectControl' +import { AutoLayoutDiv , layoutInfoGenerator } from 'public/AutoLayout' +import NavigationInPage from 'public/NavigationInPage' +import Toolkit from 'public/Toolkit/index.js' +import Button from 'bfd/Button' + +import CMDR from '../CalcManageDataRequester/requester.js' +import CalcManageConf from '../UrlConf' +import './index.less' + + +var mod = React.createClass({ + + fix(v){ + return Number(v).toFixed(4) + }, + + getInitialState(){ + return { + 'fixedTableDataList':[], + 'fixedTableHeight':0 + } + }, + componentWillMount(){ + this.initUserData() + }, + + componentDidMount(){ + this.onSearchButtonClicked() + }, + + onHeightChanged(newHeight){ + this.setState({'fixedTableHeight':newHeight}) + }, + + // 对cpu、memory、用量等数据进行进制转换、单位变换 + renderResourceUsageValue( v ){ + return this.fix(v) + ' 机器/天' + }, + // cpu数据的含义是毫核的个数,但是要求界面上展示虚拟核的个数,因此直接除以 1000 + renderCPUUsageValue(v){ + return this.fix(v/1000) + ' VCPU' + }, + renderMemoryUsageValue(v){ + let toolTipInfo = this.userData['dataFormatterInfo']['memory'] + return Toolkit.unitConversion( + v, + toolTipInfo['base'], + toolTipInfo['unitArr'], + toolTipInfo['significantFractionBit'] + ) + }, + + resetResourceUsageData(executedData){ + this.userData['resourceUsageData'] = executedData + + // 转换数据格式,方便 FixedTable 使用 + let toolTipInfo = this.userData['dataFormatterInfo'] + let fixedTableDataList = [] + + let totalBilling = 0.0 + let totalCPU = 0.0 + let totalMemory = 0.0 + + for ( let i in executedData ){ + let curData = executedData[executedData.length-1-i] + + fixedTableDataList.push({ + 'Date':curData['date'], + 'ResourceUsage':this.renderResourceUsageValue(curData['data']['request']), + 'CPUUsage':this.renderCPUUsageValue(curData['data'][toolTipInfo['cpu']['key']]), + 'MemoryUsage':this.renderMemoryUsageValue(curData['data'][toolTipInfo['memory']['key']]), + }) + totalBilling += curData['data']['request'] + totalCPU += curData['data'][toolTipInfo['cpu']['key']] + totalMemory += curData['data'][toolTipInfo['memory']['key']] + } + this.userData['fixedTableDataList'] = fixedTableDataList + this.userData['totalBilling'] = this.renderResourceUsageValue(totalBilling / executedData.length) + this.userData['totalCPU'] = this.renderCPUUsageValue(totalCPU / executedData.length) + this.userData['totalMemory'] = this.renderMemoryUsageValue(totalMemory / executedData.length) + return fixedTableDataList + }, + + initUserData(){ + this.userData = { + 'resourceUsageData':undefined, + 'keywords':['cpu','memory'] + } + + this.userData['divIDs'] = { + rootDivID:Toolkit.generateGUID(), + navigationInPageID:Toolkit.generateGUID(), + monthSelectControlID:Toolkit.generateGUID(), + billingTitleDivID:Toolkit.generateGUID(), + FixedTableFatherDivID:Toolkit.generateGUID(), + } + this.userData['autoLayoutInfos'] = [ + layoutInfoGenerator( this.userData['divIDs']['rootDivID'],true ), + layoutInfoGenerator( this.userData['divIDs']['navigationInPageID'],false,'Const' ), + layoutInfoGenerator( this.userData['divIDs']['monthSelectControlID'],false,'Const' ), + layoutInfoGenerator( this.userData['divIDs']['billingTitleDivID'],false,'Const' ), + layoutInfoGenerator( this.userData['divIDs']['FixedTableFatherDivID'],false,'Var',( newHeight )=>{ + this.onHeightChanged( newHeight ) + } ), + ] + + this.userData['fixedTableColumn'] = [ + { title:'日期', key:'Date' }, + { title:'资源用量', key:'ResourceUsage' }, + { title:'CPU用量', key:'CPUUsage' }, + { title:'Memory用量', key:'MemoryUsage' }, + ] + this.userData['dataFormatterInfo'] = { + 'cpu':{ + 'name':'CPU用量', + 'key':'cpu/request', + 'renderFunc':this.renderCPUUsageValue, + }, + 'memory':{ + 'unitArr':['B','KiB','MiB','GiB','TiB','PiB'], + 'significantFractionBit':4, + 'base':1024, + + 'name':'Memory用量', + 'key':'memory/request', + 'renderFunc':this.renderMemoryUsageValue, + }, + } + }, + + requestData( startDate,days ){ + this.setState({ 'fixedTableDataList':[] }) + CMDR.getResourceUsageInfo( CMDR.getCurNameSpace(this),startDate,days,(executedData)=>{ + this.resetResourceUsageData(executedData) + this.setState({ 'fixedTableDataList':this.userData['fixedTableDataList'] }) + }) + }, + + checkDateValid(dateStr){ + let date = new Date(dateStr) + let curDate = new Date() + if ( date.getFullYear() > curDate.getFullYear() ){ + return false + } else if ( date.getFullYear() == curDate.getFullYear() ){ + return (date.getMonth() < curDate.getMonth()) + } else { + return true + } + }, + + onSearchButtonClicked(){ + this.setState({ 'fixedTableDataList':[] }) + let dateStr = this.refs.MonthSelectControlRef.getDate() + if ( !this.checkDateValid(dateStr) ){ + message.danger( '指定月份的账单尚未生成' ) + return + } + + this.requestData( + dateStr.slice(0,'YYYY-MM-DD'.length), + Toolkit.calcMonthDays(dateStr) + ) + }, + + render: function() { + let lastMonthDate = new Date(new Date().getTime() - Toolkit.calcMonthDays(new Date())*24*60*60*1000) + let defaultSearchDateStr = Toolkit.generateTimeStrByMilliSeconds(lastMonthDate.getTime()).slice( 0,'YYYY-MM'.length ) + + let dStr = this.refs.MonthSelectControlRef ? this.refs.MonthSelectControlRef.getValue() : defaultSearchDateStr + let billingTitle = Toolkit.strFormatter.formatString('{date} {nameSpace} 账单信息',{ + 'nameSpace':CalcManageConf.getCurSpace(this), + 'date':dStr.split('-').join('年') + '月' + }) + + return ( +
+
+ +
+
+ + + + + + + + + + +
+
+
+

{billingTitle}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
空间{CalcManageConf.getCurSpace(this)}
月账单{this.userData['totalBilling']}
月平均CPU{this.userData['totalCPU']}
月平均内存{this.userData['totalMemory']}
+
+

详单

+
+
+ +
+ +
+ ) + } +}); + + +export default mod; \ No newline at end of file diff --git a/package/Aries/src/functions/CalcManage/ResourceUsageBilling/index.less b/package/Aries/src/functions/CalcManage/ResourceUsageBilling/index.less new file mode 100644 index 0000000..f7fa1e5 --- /dev/null +++ b/package/Aries/src/functions/CalcManage/ResourceUsageBilling/index.less @@ -0,0 +1,43 @@ + +div.ResourceUsageBillingRootDiv{ + + .MiddleBillingBlock{ + width: 60%; + margin-left: auto; + margin-right: auto; + } + + table.BillingOverviewInfoTable{ + font-size: 15px; + font-weight: bold; + td:nth-child(1){ width: 10px; } + td:nth-child(3){ width: 30px; } + } + + table.SearchLayoutTable{ + width: 100%; + + td:nth-child(1){ width:38% } + td:nth-child(2){ width:10% } + td:nth-child(3){ width:4% } + td:nth-child(4){ width:15% } + td:nth-child(5){ width:33% } + } + + + div.FixedTableFatherDiv { + padding-top:10px; // DataTable的表头文本距离上边框为17px + + > * { + border: 1px solid #ECECEC; + padding-top: 42px; + th > div{ + margin-top: 12px; + } + } + } + + +} + + diff --git a/package/Aries/src/functions/CalcManage/ResourceUsageRecently/index.jsx b/package/Aries/src/functions/CalcManage/ResourceUsageRecently/index.jsx new file mode 100644 index 0000000..9cf47d6 --- /dev/null +++ b/package/Aries/src/functions/CalcManage/ResourceUsageRecently/index.jsx @@ -0,0 +1,419 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import echarts from 'echarts' + +import { Tabs, TabList, Tab, TabPanel } from 'bfd/Tabs' + +import { AutoLayoutDiv , layoutInfoGenerator } from 'public/AutoLayout' +import NavigationInPage from 'public/NavigationInPage' +import Toolkit from 'public/Toolkit/index.js' +import ResourceMonitorEchart from 'public/ResourceMonitorEchart' + +import CMDR from '../CalcManageDataRequester/requester.js' +import CalcManageConf from '../UrlConf' +import './index.less' + + +var mod = React.createClass({ + + fix(v){ + return Number(v).toFixed(4) + }, + + getInitialState(){ + return { + 'echartFatherDivHeight':0, + 'totalResourceUsage':undefined, + 'totalCPU':undefined, + 'totalMemory':undefined, + } + }, + componentWillMount(){ + this.initUserData() + }, + componentDidMount(){ + this.initCharts() + this.checkToRequestData() + window.addEventListener( 'resize',this.onWindowResize ) + }, + + componentDidUpdate(){ + this.checkToRequestData() + }, + + onWindowResize(){ + window.removeEventListener( 'resize',this.onWindowResize ) + for ( let i in this.userData['keywords'] ){ + let obj = this.userData['echartObjs'][ this.userData['keywords'][i] ] + obj && obj.resize() + } + + window.addEventListener( 'resize',this.onWindowResize ) + }, + + checkToRequestData(){ + let curNameSpace = CMDR.getCurNameSpace(this) + if ( this.curNameSpace !== curNameSpace ){ + this.curNameSpace = curNameSpace + this.requestData() + } + }, + + resetrequestedUsageData(executedData,recentDaysStartDate,monthFirstDate){ + this.userData['requestedUsageData'] = executedData + let echartBaseInfo = this.userData['echartBaseInfo'] + + // 汇总echart的x、y轴数据 + let dateKeys = [] + let resourceUsageValues = [] + let cpuUsageValues = [] + let memoryUsageValues = [] + for ( let index = 0 ; index < executedData.length ; index ++ ){ + // 走势图显示近14天的数据 + if ( recentDaysStartDate.getTime() <= new Date(executedData[index]['date']).getTime() ){ + dateKeys.push( executedData[index]['date'] ) + resourceUsageValues.push( executedData[index]['data'][ echartBaseInfo['resourceUsage']['key'] ] ) + cpuUsageValues.push( executedData[index]['data'][ echartBaseInfo['cpu']['key'] ] ) + memoryUsageValues.push( executedData[index]['data'][ echartBaseInfo['memory']['key'] ] ) + } + } + this.userData['echartData']['resourceUsage'] = { + 'xAxisData':dateKeys, + 'seriesData':resourceUsageValues + } + this.userData['echartData']['cpu'] = { + 'xAxisData':dateKeys, + 'seriesData':cpuUsageValues + } + this.userData['echartData']['memory'] = { + 'xAxisData':dateKeys, + 'seriesData':memoryUsageValues + } + + let days = 0.0 + // 计算出来本月的CPU、内存、用量总和 + let totalResourceUsage = 0.0 + let totalCPU = 0.0 + let totalMemory = 0.0 + + for ( let i in executedData ){ + let curData = executedData[executedData.length-1-i] + // 汇总数据显示月初到当天的汇总 + if ( monthFirstDate.getTime() <= new Date(curData['date']).getTime() ){ + days += 1 + totalResourceUsage += curData['data'][ echartBaseInfo['resourceUsage']['key'] ] + totalCPU += curData['data'][ echartBaseInfo['cpu']['key'] ] + totalMemory += curData['data'][ echartBaseInfo['memory']['key'] ] + } + } + + this.setState({'totalResourceUsage':totalResourceUsage/days}) + this.setState({'totalCPU':totalCPU/days}) + this.setState({'totalMemory':totalMemory/days}) + }, + + initUserData(){ + this.userData = {} + + // AutoLayout 需要使用的一些信息 + this.userData['divIDs'] = { + 'rootDivID':Toolkit.generateGUID(), + 'navigationInPageID':Toolkit.generateGUID(), + 'recentlyTitleDivID':Toolkit.generateGUID(), + 'echartFatherDivID':Toolkit.generateGUID(), + } + this.userData['autoLayoutInfos'] = [ + layoutInfoGenerator( this.userData['divIDs']['rootDivID'],true ), + layoutInfoGenerator( this.userData['divIDs']['navigationInPageID'],false,'Const' ), + layoutInfoGenerator( this.userData['divIDs']['recentlyTitleDivID'],false,'Const' ), + layoutInfoGenerator( this.userData['divIDs']['echartFatherDivID'],false,'Var',( newHeight )=>{ + this.setState({'echartFatherDivHeight':newHeight}) + } ), + ] + + // echart保存的一些信息 + this.userData['echartObjs'] = {} + this.userData['keywords'] = ['resourceUsage','cpu','memory'] + this.userData['recentlyDays'] = 14 + this.userData['requestedUsageData'] = undefined // 它保存了从后台请求到的原始数据 + + this.userData['echartBaseInfo'] = { + 'resourceUsage':{ + 'name':'资源用量', + 'key':'request', + 'renderFunc':this.renderResourceUsageValue, + 'divID':'ResourceUsageEchartDiv', + 'tooltipFormatterFunc':undefined, + 'yAxisLabelFormatterFunc':undefined, + }, + 'cpu':{ + 'name':'CPU用量', + 'key':'cpu/request', + 'renderFunc':this.renderCPUUsageValue, + 'divID':'CPUEchartDiv', + 'tooltipFormatterFunc':undefined, + 'yAxisLabelFormatterFunc':undefined, + }, + 'memory':{ + 'name':'Memory用量', + 'key':'memory/request', + 'renderFunc':this.renderMemoryUsageValue, + 'divID':'MemoryEchartDiv', + 'tooltipFormatterFunc':undefined, + 'yAxisLabelFormatterFunc':undefined, + + 'unitArr':['B','KiB','MiB','GiB','TiB','PiB'], + 'significantFractionBit':4, + 'base':1024, + }, + } + + // 从后台请求到的数据并进行处理之后,会放到这里 + this.userData['echartData'] = { + 'resourceUsage':{}, + 'cpu':{}, + 'memory':{}, + } + + // 用来渲染数据的单位的函数 + this.userData['echartBaseInfo']['resourceUsage']['renderFunc'] = (v)=>{ return this.fix(v) + ' 机器/天' } + // cpu数据的含义是毫核的个数,但是要求界面上展示虚拟核的个数,因此直接除以 1000 + this.userData['echartBaseInfo']['cpu']['renderFunc'] = (v)=>{ return this.fix(v/1000) + ' VCPU' } + this.userData['echartBaseInfo']['memory']['renderFunc'] = (v)=>{ + let echartBaseInfo = this.userData['echartBaseInfo']['memory'] + return Toolkit.unitConversion( + v, + echartBaseInfo['base'], + echartBaseInfo['unitArr'], + echartBaseInfo['significantFractionBit'] + ) + } + + // 用来渲染echart的tooltip的回调函数 + let tooltipFormatterFunc = (echartBaseInfo,x,y,yLabel)=>{ + if ( !this.userData['requestedUsageData'] ){ + return + } + return Toolkit.strFormatter.formatString( this.generateTooltipFormatterStr(1),{ + 'time':x, + 'name0':yLabel, + 'value0':echartBaseInfo['renderFunc'](y) + }) + } + this.userData['echartBaseInfo']['resourceUsage']['tooltipFormatterFunc'] = ( params, ticket, callback ) => { + let echartBaseInfo = this.userData['echartBaseInfo']['resourceUsage'] + return tooltipFormatterFunc( echartBaseInfo,params[0]['name'],params[0]['data'],params[0]['seriesName'] ) + } + this.userData['echartBaseInfo']['cpu']['tooltipFormatterFunc'] = ( params, ticket, callback ) => { + let echartBaseInfo = this.userData['echartBaseInfo']['cpu'] + return tooltipFormatterFunc( echartBaseInfo,params[0]['name'],params[0]['data'],params[0]['seriesName'] ) + } + this.userData['echartBaseInfo']['memory']['tooltipFormatterFunc'] = ( params, ticket, callback ) => { + let echartBaseInfo = this.userData['echartBaseInfo']['memory'] + return tooltipFormatterFunc( echartBaseInfo,params[0]['name'],params[0]['data'],params[0]['seriesName'] ) + } + + // 用来渲染echart的y轴标签的回调函数 + this.userData['echartBaseInfo']['resourceUsage']['yAxisLabelFormatterFunc'] = ( value, index ) => { + return this.userData['echartBaseInfo']['resourceUsage']['renderFunc'](value) + } + this.userData['echartBaseInfo']['cpu']['yAxisLabelFormatterFunc'] = ( value, index ) => { + return this.userData['echartBaseInfo']['cpu']['renderFunc'](value) + } + this.userData['echartBaseInfo']['memory']['yAxisLabelFormatterFunc'] = ( value, index ) => { + return this.userData['echartBaseInfo']['memory']['renderFunc'](value) + } + }, + + initCharts(){ + let colorPool = ['rgba(38,166,154,0.9)','rgba(255,138,101,0.9)','rgba(102,187,106,0.9)'] + + for ( let i in this.userData['keywords'] ){ + let k = this.userData['keywords'][i] + let echartBaseInfo = this.userData['echartBaseInfo'][k] + + let initOptions = this.generateInitEchartOption(echartBaseInfo['name'],echartBaseInfo['tooltipFormatterFunc'],echartBaseInfo['yAxisLabelFormatterFunc']) + initOptions['color'] = [colorPool[i]] + + this.userData['echartObjs'][k] = echarts.init(document.getElementById( echartBaseInfo['divID'] )) + this.userData['echartObjs'][k].setOption( initOptions ) + } + }, + + requestData(){ + // 该页面需要两块数据:账单信息要求显示本月初到今天的数据。走势图需要近14天的数据,为了一次请求成功,因此需要把这两个时间段的数据汇总起来 + let todayStartStr = Toolkit.generateTimeStrByMilliSeconds(-1).slice(0,'YYYY-MM-DD'.length) + let sDate1 = new Date(new Date(todayStartStr).getTime() - (this.userData['recentlyDays'])*24*60*60*1000) // 最近14天 + let sDate2 = new Date(todayStartStr.slice(0,'YYYY-MM'.length)+'-01') // 月初 + + let days = undefined + let sDate = undefined + if ( sDate1.getTime() < sDate2.getTime() ){ + sDate = sDate1 + days = this.userData['recentlyDays'] + } else { + sDate = sDate2 + days = (new Date(todayStartStr).getDate())-1 + } + let startDateStr = Toolkit.generateTimeStrByMilliSeconds(sDate.getTime()).slice(0,'YYYY-MM-DD'.length) + + for ( let i in this.userData['keywords'] ){ + this.userData['echartObjs'][ this.userData['keywords'][i] ].showLoading() + } + CMDR.getResourceUsageInfo(CMDR.getCurNameSpace(this),startDateStr,days,(executedData)=>{ + this.resetrequestedUsageData(executedData,sDate1,sDate2) + this.insertDataToEchart() + for ( let i in this.userData['keywords'] ){ + this.userData['echartObjs'][ this.userData['keywords'][i] ].hideLoading() + } + }) + }, + + insertDataToEchart(){ + if (!this.userData['requestedUsageData']){ + return + } + for ( let i in this.userData['keywords'] ){ + let k = this.userData['keywords'][i] + this.userData['echartObjs'][k].setOption({ + 'legend':{ + 'data':[this.userData['echartBaseInfo'][k]['name']] + }, + 'xAxis': { + 'type' : 'category', + 'data': this.userData['echartData'][k]['xAxisData'] + }, + 'series': [{ + 'type': 'bar', + 'name':this.userData['echartBaseInfo'][k]['name'], + 'data':this.userData['echartData'][k]['seriesData'], + }] + }) + } + }, + + generateInitEchartOption( title,tooltipFormatterFunc,yAxisLabelFormatterFunc ){ + return { + 'title': { + 'text': title + }, + toolbox: { + 'show':true, + 'feature':{ + 'mark':{ + 'show': true + }, + 'magicType':{ + 'show':true, + 'type':['line', 'bar'] + }, + } + }, + 'tooltip' : { + 'trigger': 'axis', + 'formatter': tooltipFormatterFunc + }, + grid:{ + 'x': 120 + }, + 'xAxis': [{ + 'type' : 'category', + 'data': [] + }], + 'yAxis':{ + 'axisLabel':{ + 'show':true, + 'formatter':yAxisLabelFormatterFunc + } + }, + } + }, + + generateTooltipFormatterStr( lineNumber ){ + let templateStr = '{time}' + for ( let i = 0 ; i < lineNumber ; i ++ ){ + templateStr += '' + templateStr += '{name'+i+'}' + templateStr += '' + templateStr += '{value'+i+'}' + templateStr += '' + } + return '' + templateStr + '
' + }, + + render: function() { + + let monthFirstDayStr = Toolkit.generateTimeStrByMilliSeconds((new Date()).getTime()).slice( 0,'YYYY-MM'.length ) + let recentlyTitle = Toolkit.strFormatter.formatString('{date} {nameSpace} 账单信息(截至到当天凌晨)',{ + 'nameSpace':CalcManageConf.getCurSpace(this), + 'date':monthFirstDayStr.split('-').join('年') + '月' + }) + + let picTitle = Toolkit.strFormatter.formatString('近 {days} 天资源走势图',{'days':this.userData['recentlyDays']}) + + let totalResourceUsage = this.state.totalResourceUsage != undefined ? this.userData['echartBaseInfo']['resourceUsage']['renderFunc'](this.state.totalResourceUsage) : '' + let totalCPU = this.state.totalCPU != undefined ? this.userData['echartBaseInfo']['cpu']['renderFunc'](this.state.totalCPU) : '' + let totalMemory = this.state.totalMemory != undefined ? this.userData['echartBaseInfo']['memory']['renderFunc'](this.state.totalMemory) : '' + + return ( +
+
+ +
+
+

{recentlyTitle}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
空间{CalcManageConf.getCurSpace(this)}
月账单{totalResourceUsage}
月平均CPU{totalCPU}
月平均内存{totalMemory}
+
+

{picTitle}

+
+
+
+
+
+
+
+
+ +
+ ) + } +}); + + +export default mod; \ No newline at end of file diff --git a/package/Aries/src/functions/CalcManage/ResourceUsageRecently/index.less b/package/Aries/src/functions/CalcManage/ResourceUsageRecently/index.less new file mode 100644 index 0000000..7150e0e --- /dev/null +++ b/package/Aries/src/functions/CalcManage/ResourceUsageRecently/index.less @@ -0,0 +1,42 @@ + +div.ResourceUsageRecentlyRootDiv{ + + table.TooltipTable td.SpaceTdDistraction { + width: 10px; + } + + table.RecentlyOverviewInfoTable{ + font-size: 15px; + font-weight: bold; + td:nth-child(1){ width: 10px; } + td:nth-child(3){ width: 30px; } + } + + .MiddleMainBlock{ + width: 80%; + margin-left: auto; + margin-right: auto; + } + + div.EchartFatherDiv{ + overflow-x: hidden; + overflow-y: auto; + margin:auto; + width: 80%; + + div#ResourceUsageEchartDiv{ + margin-top: 30px; + margin:auto; + width:85%; + height:400px; + } + div#CPUEchartDiv,div#MemoryEchartDiv{ + display: inline-block; + margin:auto; + width:50%; + height:280px; + } + } +} + + diff --git a/package/Aries/src/public/Conf/Conf.js b/package/Aries/src/public/Conf/Conf.js index ac27363..e4e62ed 100644 --- a/package/Aries/src/public/Conf/Conf.js +++ b/package/Aries/src/public/Conf/Conf.js @@ -337,6 +337,20 @@ const conf = { url: "/CloudContainer/CalcManage/Overview?cur_space=${spaceName}" }] }, + ResourceUsageRecently: { + headText: "资源用量-近期", + navigationTexts: [{ + text: "资源用量-近期", + url: "/CloudContainer/CalcManage/ResourceUsageRecently?cur_space=${spaceName}" + }] + }, + ResourceUsageBilling: { + headText: "资源用量-账单", + navigationTexts: [{ + text: "资源用量-账单", + url: "/CloudContainer/CalcManage/ResourceUsageBilling?cur_space=${spaceName}" + }] + }, PodInfo: { headText: "Pod信息", navigationTexts: [{ diff --git a/package/Aries/src/public/MonthSelectControl/index.jsx b/package/Aries/src/public/MonthSelectControl/index.jsx new file mode 100644 index 0000000..774f76a --- /dev/null +++ b/package/Aries/src/public/MonthSelectControl/index.jsx @@ -0,0 +1,178 @@ + +import React from 'react' +import ReactDOM from 'react-dom' + +import { Dropdown, DropdownToggle, DropdownMenu } from 'bfd/Dropdown' +import { Select, Option } from 'bfd/Select' +import Input from 'bfd/Input' +import Button from 'bfd/Button' + +import './index.less' + +/* +月份选择控件: +输入: + years 年份数组,比如 [2015,2016,2017] ,如果不传,则将会默认展示过去四年的年份 + defaultValue 默认年月,比如 '2016-11' ,如果不传,则使用当前的年月 + onChange 当用户更改日期的时候的回调函数 +输出: + 可以通过该组建的 ref 来调用 getValue 函数,即可返回用户选择的年月,返回的格式为: 2016-12(字符串) + 也可以通过该组建的 ref 来调用 getDate 函数,即可返回用户选择的年月所对应的Date对象(该年月第一天的凌晨) + +*/ + +var MonthSelectControl = React.createClass({ + + getInitialState(){ + return { + 'dropdownOpenState':false, + 'hasBindButtonCallBack':false, + 'inputValue':'', + } + }, + + executePropInfo( propYears,propDefaultDateStr ){ + + let years = [] + if ( propYears ){ + years = propYears + } else { + let curYear = ( new Date() ).getFullYear() + for ( let i = 3 ; i >= 0 ; i -- ){ + years.push( curYear-i ) + } + } + + let defaultDate = propDefaultDateStr ? (new Date(propDefaultDateStr)) : (new Date()) + let defaultYear = defaultDate.getFullYear() + if ( years.indexOf( defaultYear ) == -1 ){ + defaultYear = years[years.length-1] + } + + return { + 'years':years, + 'defaultYear':defaultYear, + 'defaultMonth':defaultDate.getMonth()+1 + } + }, + + componentWillMount(){ + this.userData = {} + + this.userData['monthInfoList'] = [ + ['01','02','03','04'], + ['05','06','07','08'], + ['09','10','11','12'] + ] + + this.userData['executedPropInfos'] = this.executePropInfo(this.props.years,this.props.defaultValue) + this.state.inputValue = this.combineDateInfo( + this.userData['executedPropInfos']['defaultYear'], + this.userData['executedPropInfos']['defaultMonth'] + ) + + this.userData['selectInfos'] = { + 'selectedYear':undefined, + 'selectedMonth':undefined, + } + }, + + onDropdownToggle(state){ + state && setTimeout( ()=>{ this.bindButtonClickCallBack() },500 ) + }, + + // 由于在 onDropdownToggle 函数中不能保证 DropdownMenu 的内容已经被渲染到页面上,因此需要存储月份按钮是否绑定click事件的状态 + bindButtonClickCallBack(){ + if ( this.userData['hasBindButtonCallBack'] != true ){ + this.userData['monthInfoList'].map( (curLine)=>{ + curLine.map( (monthIndex)=>{ + ReactDOM.findDOMNode( this.refs['MonthButtonRef'+monthIndex] ).addEventListener( 'click',()=>{ + this.onMonthButtonClicked( monthIndex ) + this.refs.PleaseSelectMonthControlDropDownRef.close() + } ) + } ) + } ) + this.userData['hasBindButtonCallBack'] = true + } + }, + + onMonthButtonClicked(month){ + this.userData['selectedMonth'] = month + this.refreshInputValue() + }, + + onYearSelectValueChanged(year){ + this.userData['selectedYear'] = year + this.refreshInputValue() + }, + + refreshInputValue(){ + let dateStr = this.getValue() + this.setState({'inputValue':dateStr}) + this.props.onChange && this.props.onChange( dateStr ) + }, + + combineDateInfo(year,month){ + year = Number(year) + month = Number(month) + month = (month < 10) ? '0'+month : ''+month + return year+'-'+month + }, + + getValue(){ + return this.combineDateInfo( + this.userData['selectedYear'] ? this.userData['selectedYear'] : this.userData['executedPropInfos']['defaultYear'], + this.userData['selectedMonth'] ? this.userData['selectedMonth'] : this.userData['executedPropInfos']['defaultMonth'] + ) + }, + getDate(){ + return this.getValue() + '-01 00:00:00' + }, + + render:function (){ + + return ( +
+ + + + + + + + + + + {this.userData['monthInfoList'].map( (curLine)=>{ + return ( + + {curLine.map( (curMonthNumber)=>{ + return ( + + ) + })} + + ) + })} + +
+ +
+
{curMonthNumber}
+
+
+
+
+ + ) + } +}) + +export default MonthSelectControl + diff --git a/package/Aries/src/public/MonthSelectControl/index.less b/package/Aries/src/public/MonthSelectControl/index.less new file mode 100644 index 0000000..259756d --- /dev/null +++ b/package/Aries/src/public/MonthSelectControl/index.less @@ -0,0 +1,67 @@ + +div.MonthSelectControlRootDiv { + +} + + + +// 由于DropDown实现的原因,在html节点树上,MonthButton 并不是 MonthSelectControlRootDiv 的子孙节点 + +table.MonthSelectTable{ + @buttonSideLength: 24px; + @buttonMargin: 2px; + + // 下拉框的最小宽度要重新调整一下 + tr:nth-child(1) > td { + > *{ + min-width: 4*(@buttonSideLength+2*@buttonMargin); + max-width: 4*(@buttonSideLength+2*@buttonMargin); + margin-bottom: 6px; + } + } + + tr{ + margin: 0px; + padding: 0px; + td{ + width: @buttonSideLength+2*@buttonMargin; + max-width: @buttonSideLength+2*@buttonMargin; + min-width: @buttonSideLength+2*@buttonMargin; + + height: @buttonSideLength+2*@buttonMargin; + max-height: @buttonSideLength+2*@buttonMargin; + min-height: @buttonSideLength+2*@buttonMargin; + } + } + + tr td { + div.MonthButton{ + font-weight: bold; + cursor: pointer; + + margin: @buttonMargin; + + width: @buttonSideLength; + min-width: @buttonSideLength; + max-width: @buttonSideLength; + height: @buttonSideLength; + min-height: @buttonSideLength; + max-height: @buttonSideLength; + line-height: @buttonSideLength; + + text-align: center; + + border: 1px solid #42a5f5; + border-radius: 2px; + } + div.MonthButton{ + color: #42a5f5; + background-color: white; + } + div.MonthButton:hover{ + color: white; + background-color: #42a5f5; + } + + } +} diff --git a/package/Aries/src/public/Toolkit/index.js b/package/Aries/src/public/Toolkit/index.js index 87fe445..9df84e0 100644 --- a/package/Aries/src/public/Toolkit/index.js +++ b/package/Aries/src/public/Toolkit/index.js @@ -2,6 +2,24 @@ var Toolkit = { + // 计算某年某月共多少天 + calcMonthDays(date){ + if (date){ + date = new Date(date) + } else { + date = new Date() + } + let biggerDays = 0 + while ( date.getMonth() == new Date(date.getTime()+biggerDays*24*60*60*1000).getMonth() ){ + biggerDays += 1 + } + let smallerDays = 0 + while ( date.getMonth() == new Date(date.getTime()-smallerDays*24*60*60*1000).getMonth() ){ + smallerDays += 1 + } + return biggerDays+smallerDays-1 + }, + // 传入开始时间字符串,计算出开始时间距离现在大约多长时间 calcAge(createTime){ if ( !createTime ){ diff --git a/package/Aries/src/router.jsx b/package/Aries/src/router.jsx index a08934d..54a7378 100644 --- a/package/Aries/src/router.jsx +++ b/package/Aries/src/router.jsx @@ -104,6 +104,17 @@ export default render(( cb(null, require('./functions/CalcManage/ClusterInfo/ingressinfo').default) }) }}/> + { + require.ensure([], require => { + cb(null, require('./functions/CalcManage/ResourceUsageRecently').default) + }) + }}/> + { + require.ensure([], require => { + cb(null, require('./functions/CalcManage/ResourceUsageBilling').default) + }) + }}/> + {