首页 > 其他 > 详细

K8s之Helm工具详解

时间:2020-09-01 22:33:44      阅读:63      评论:0      收藏:0      [点我收藏+]
一、helm介绍
  • 在Kubernetes中部署容器云的应用也是一项有挑战性的工作,Helm就是为了简化在Kubernetes中安装部署容器云应用的一个客户端工具。通过helm能够帮助开发者定义、安装和升级Kubernetes中的容器云应用,同时,也可以通过helm进行容器云应用的分享。在Kubeapps Hub中提供了包括Redis、MySQL和Jenkins等常见的应用,通过helm可以使用一条命令就能够将其部署安装在自己的Kubernetes集群中。

  • Helm是一个用于kubernetes的包管理器。每个包称为一个Chart,一个Chart是一个目录(一般情况下会将目录进行打包压缩,形成name-version.tgz格式的单一文件,方便传输和存储)。我们可以将Helm看作Kubernetes下的apt-get/yum。

  • 对于应用发布者而言,可以通过Helm打包应用,管理应用依赖关系,管理应用版本并发布应用到软件仓库。

  • 对于使用者而言,使用Helm后无须了解Kubernetes的yaml文件语法、应用部署文件。直接使用Helm下载即可在Kubernetes上安装需要的应用。

Helm组件及相关术语

1)Helm
Helm是一个命令行下的客户端工具。主要用于Kubernetes应用程序Chart的创建、打包、发布及创建和管理
本地和远程的Chart仓库。

2)Tiller
Tiller是Helm的服务端,部署在Kubernetes集群中,Tiller用于接收Helm的请求,并根据Chart生成Kubernete
s的部署文件,然后提交给Kubernetes创建应用,Tiller还提供和了Release的升级、删除、回滚等一系列功能。

3)Chart
Chart是一个Helm的程序包,包含了运行一个Kubernetes应用程序所需的镜像、依赖关系和资源定义等。

4)Repoistory
Repoistory是Helm的软件仓库,Repository本质上是一个Web服务器,该服务器保存了一系列的Chart软件包
以供用户下载,并且提供了该Repository的Chart包的清单文件便于查询。Helm可以同时管理多个不同的Repository。

5)Release
Release是应用程序运行Chart之后,得到的一个实例。

elm架构
helm的整体架构如下图所示,Helm架构由Helm客户端、Tiller服务器端和Chart仓库所组成;Tiller部署在Kubernetes中,Helm客户端从Chart仓库中获取Chart安装包,并将其安装部署到Kubernetes集群中。
技术分享图片

Chart install过程:
1)Helm从指定的目录或者tgz文件中解析出Chart结构信息;
2)Helm将指定的Chart结构和Values信息通过gRPC传递给Tiller;
3)Tiller根据Chart和Values生成一个Release;
4)Tiller将Release发送给Kubernetes用于生成Release;
---
Chart update过程:
1)Helm从指定的目录或者tgz文件中解析出Chart结构信息;
2)Helm将要更新的Release的名称和Chart结构、Values信息传递给Tiller;
3)Tiller生成Release并更新指定名称的Release的history;
4)Tiller将Release发送给Kubernetes用于更新Release;
---
Chart Rollback过程:
1)Helm将要回滚的Release的名称传递给Tiller;
2)Tiller根据Release的名称查找history;
3)Tiller从history中获取上一个Release;
4)Tiller将上一个Release发送给Kubernetes用于替换当前的Release;

二、部署Helm

1)安装Helm客户端

[root@master ~]# wget https://get.helm.sh/helm-v2.14.3-linux-amd64.tar.gz
//从Github上下载Helm软件包
[root@master ~]# tar zxf helm-v2.14.3-linux-amd64.tar.gz
[root@master ~]# mv linux-amd64/helm /usr/local/bin/
[root@master ~]# chmod +x /usr/local/bin/helm 
//获取这个软件包只是为了获取这个helm文件
[root@master ~]# echo ‘source <(helm completion bash)‘ >> /etc/profile
[root@master ~]# source /etc/profile
//设置helm命令可以自动补全并且写入环境变量文件中

2)安装Tiller server(需要创建授权用户)

[root@master ~]#  vim tiller-rbac.yaml             //编写yaml文件并创建授权用户
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system
[root@master ~]# kubectl apply -f tiller-rbac.yaml  
[root@master ~]# helm init --service-account=tiller
//Tiller server的环境初始化
//helm的服务端就是Tiller
//指定刚才创建的授权用户初始化helm

[root@master ~]# kubectl get pod -n kube-system | grep tiller  //查看tiller的pod名称
tiller-deploy-8557598fbc-mlr6c   0/1     ContainerCreating   0          6s
//编辑pod的yaml文件,将其使用的镜像改为国内阿里云的,默认是Google的镜像,下载不下来
//修改spec字段的image指定的镜像,如下:

[root@master ~]# kubectl  edit  pod -n kube-system  tiller-deploy-8557598fbc-mlr6c
//修改spec字段的image指定的镜像,如下:
    image: gcr.io/kubernetes-helm/tiller:v2.14.3   
//修改后如下:
    image: registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.14.3
//修改后,保存退出即可,它会去自动下载新镜像(如果没有自动下载,就想办法吧,比如说在tiller容器所在的节点手动下载下来镜像,然后重启该节点的kubelet,或重启该容器)

[root@master ~]#  kubectl get pod -n kube-system  | grep tiller  //必须保证tiller的Pod正常运行
tiller-deploy-8557598fbc-mlr6c   1/1     Running   0          5m16s

注:避免出错附加一张图片如下:
技术分享图片

3)配置Helm仓库

[root@master ~]# helm repo list   //查看仓库的详细信息
NAME    URL                                             
stable  https://kubernetes-charts.storage.googleapis.com   //默认是Google,在国外,速度特别慢
local   http://127.0.0.1:8879/charts   

[root@master ~]#  helm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
//更改为国内阿里云的地址

[root@master ~]# helm repo list 
//再次查看仓库的详细信息,可以发现地址已经换成了阿里云的仓库地址
NAME    URL                                                   
stable  https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
local   http://127.0.0.1:8879/charts   

[root@master ~]# helm repo update  //更新一下helm仓库
[root@master ~]# helm version        //查看helm版本信息
Client: &version.Version{SemVer:"v2.14.3", GitCommit:"0e7f3b6637f7af8fcfddb3d2941fcc7cbebb0085", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.14.3", GitCommit:"0e7f3b6637f7af8fcfddb3d2941fcc7cbebb0085", GitTreeState:"clean"}
//必须保证可以查看出来client和server,才可正常使用helm

注:就此Helm就已经表示部署成功,可以正常使用!

三、使用helm安装mysql+Storage Class

创建Storage Class
1)部署nfs

[root@master ~]# yum -y install nfs-utils rpcbind
[root@master ~]# mkdir /nfsdata
[root@master ~]# echo "/nfsdata *(rw,no_root_squash,sync)" > /etc/exports
[root@master ~]# systemctl start nfs-server && systemctl start rpcbind
[root@master ~]# showmount -e
Export list for master:
/nfsdata *

2)创建rbac授权

[root@master ~]# vim rbac-rolebind.yaml   //编辑yaml文件
apiVersion: v1                            //创建一个用于认证的服务账号
kind: ServiceAccount
metadata:
  name: nfs-provisioner
---
apiVersion: rbac.authorization.k8s.io/v1        //创建群集规则
kind: ClusterRole
metadata:
  name: nfs-provisioner-runner
rules:
   -  apiGroups: [""]
      resources: ["persistentvolumes"]
      verbs: ["get", "list", "watch", "create", "delete"]
   -  apiGroups: [""]
      resources: ["persistentvolumeclaims"]
      verbs: ["get", "list", "watch", "update"]
   -  apiGroups: ["storage.k8s.io"]
      resources: ["storageclasses"]
      verbs: ["get", "list", "watch"]
   -  apiGroups: [""]
      resources: ["events"]
      verbs: ["watch", "create", "update", "patch"]
   -  apiGroups: [""]
      resources: ["services", "endpoints"]
      verbs: ["get","create","list", "watch","update"]
   -  apiGroups: ["extensions"]
      resources: ["podsecuritypolicies"]
      resourceNames: ["nfs-provisioner"]
      verbs: ["use"]
---
kind: ClusterRoleBinding                //将服务认证用户与群集规则进行绑定
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-provisioner
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-provisioner-runner
  apiGroup: rbac.authorization.k8s.io

[root@master ~]# kubectl apply -f rbac-rolebind.yaml  //执行yaml文件

3)创建nfs-deployment.资源

[root@master ~]# vim nfs-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1                              //指定副本数量为1
  strategy:
    type: Recreate                      //指定策略类型为重置
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccount: nfs-provisioner            //指定rbac yaml文件中创建的认证用户账号
      containers:
        - name: nfs-client-provisioner
          image: registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner  //使用的镜像 
          volumeMounts:
            - name: nfs-client-root
              mountPath:  /persistentvolumes             //指定容器内挂载的目录
          env:
            - name: PROVISIONER_NAME           //容器内的变量用于指定提供存储的名称
              value: bjq-test
            - name: NFS_SERVER                      //容器内的变量用于指定nfs服务的IP地址
              value: 192.168.45.129
            - name: NFS_PATH                       //容器内的变量指定nfs服务器对应的目录
              value: /nfsdata
      volumes:                                                //指定挂载到容器内的nfs的路径及IP
        - name: nfs-client-root
          nfs:
            server: 192.168.45.129
            path: /nfsdata
        //编辑完成保存退出即可

[root@master ~]# kubectl  apply  -f nfs-deployment.yaml   //执行yaml文件
[root@master ~]# kubectl  get  pod | grep nfs  //确认pod是否正常运行
nfs-client-provisioner-8844fb875-prjhq   1/1     Running   0          14s

4)创建Storage Class

[root@master ~]# vim storage-class.yaml   //编辑yaml文件
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: stateful-nfs
provisioner: bjq-test    //这个要和nfs-client-provisioner的env环境变量中的PROVISIONER_NAME的value值对应。
reclaimPolicy: Retain  //指定回收策略为Retain(手动释放)

[root@master ~]# kubectl apply -f storage-class.yaml   //执行yaml文件
[root@master ~]# kubectl get sc            //确认Storage Class创建成功  
NAME           PROVISIONER   AGE
stateful-nfs   bjq-test      8s

5)helm部署mysql

[root@master ~]# helm fetch stable/mysql       //将mysql的软件包下载到本地
[root@master ~]# tar zxf mysql-0.3.5.tgz 

[root@master ~]# vim mysql/values.yaml     +55
  storageClass: "stateful-nfs"                  //去除注释,并指定刚才创建的Storage Class名称

[root@master ~]# vim mysql/values.yaml +79
  type: NodePort                                     //默认是ClusterIP,可更改为NodePort

[root@master ~]# helm install stable/mysql -f mysql/values.yaml --set mysqlRootPassword=123.com -n test-mysql
//如果修改了values.yaml文件的数据,创建时应该指定values.yaml文件  
//安装mysql,并设置mysql的root密码为123.com,-n表示指定名称

root@master ~]# helm list               //查看已安装的软件包
NAME        REVISION    UPDATED                     STATUS      CHART       APP VERSION NAMESPACE
test-mysql  1           Tue Sep  1 13:26:01 2020    DEPLOYED    mysql-0.3.5             default  

避免出错附加一张图片:
技术分享图片

[root@master ~]# kubectl get svc | grep test-mysql //确认service类型
test-mysql-mysql   NodePort    10.98.129.4   <none>        3306:30083/TCP   4m56s

[root@master ~]# kubectl get pv,pvc  //查看PV、PVC是否绑定成功
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS   REASON   AGE
persistentvolume/pvc-4ebb3a43-b192-4e2d-884a-2b75308f63dd   8Gi        RWO            Delete           Bound    default/test-mysql-mysql   stateful-nfs            5m5s

NAME                                     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/test-mysql-mysql   Bound    pvc-4ebb3a43-b192-4e2d-884a-2b75308f63dd   8Gi        RWO            stateful-nfs   5m5s

[root@master ~]# kubectl  exec  -it test-mysql-mysql-dfb9b6944-q7tcd  -- mysql -u root -p123.com
//登录到刚才创建的mysql数据库中
mysql> 

6)使用helm对mysql进行升级操作

[root@master ~]# kubectl  describe  pod test-mysql-mysql-dfb9b6944-q7tcd 
//查看pod的详细信息以下可以看出,该pod所用镜像为mysql:5.7.14
Events:
  Type     Reason            Age                    From               Message
  ----     ------            ----                   ----               -------
  Warning  FailedScheduling  8m57s (x2 over 8m57s)  default-scheduler  pod has unbound immediate PersistentVolumeClaims (repeated 2 times)
  Normal   Scheduled         8m56s                  default-scheduler  Successfully assigned default/test-mysql-mysql-dfb9b6944-q7tcd to node02
  Normal   Pulling           8m55s                  kubelet, node02    Pulling image "busybox:1.25.0"
  Normal   Pulled            8m46s                  kubelet, node02    Successfully pulled image "busybox:1.25.0"
  Normal   Created           8m46s                  kubelet, node02    Created container remove-lost-found
  Normal   Started           8m46s                  kubelet, node02    Started container remove-lost-found
  Normal   Pulling           8m46s                  kubelet, node02    Pulling image "mysql:5.7.14"
  Normal   Pulled            8m24s                  kubelet, node02    Successfully pulled image "mysql:5.7.14"
  Normal   Created           8m24s                  kubelet, node02    Created container test-mysql-mysql
  Normal   Started           8m24s                  kubelet, node02    Started container test-mysql-mysql

[root@master ~]# kubectl get pod | grep test-mysql  //注意升级之后,pod名称会发生变化
test-mysql-mysql-54c7bfdd77-8jlpj        1/1     Running   0          2m54s

[root@master ~]# kubectl  describe  pod test-mysql-mysql-54c7bfdd77-8jlpj  
//再次查看pod详细信息,可以看出,镜像升级为mysql:5.7.15
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  4m1s   default-scheduler  Successfully assigned default/test-mysql-mysql-54c7bfdd77-8jlpj to node01
  Normal  Pulling    4m1s   kubelet, node01    Pulling image "busybox:1.25.0"
  Normal  Pulled     3m50s  kubelet, node01    Successfully pulled image "busybox:1.25.0"
  Normal  Created    3m50s  kubelet, node01    Created container remove-lost-found
  Normal  Started    3m50s  kubelet, node01    Started container remove-lost-found
  Normal  Pulling    3m50s  kubelet, node01    Pulling image "mysql:5.7.15"
  Normal  Pulled     3m25s  kubelet, node01    Successfully pulled image "mysql:5.7.15"
  Normal  Created    3m25s  kubelet, node01    Created container test-mysql-mysql
  Normal  Started    3m25s  kubelet, node01    Started container test-mysql-mysql

7)使用helm对mysql进行回滚操作

[root@master ~]# helm list
//再次查看实例,可以发现“REVISION”字段为2,而初次创建实例时,该列为1,只要对该实例进行升级回滚一次,数值便会加1
NAME        REVISION    UPDATED                     STATUS      CHART       APP VERSION NAMESPACE
test-mysql  2           Tue Sep  1 13:37:45 2020    DEPLOYED    mysql-0.3.5             default 

[root@master ~]# helm history  test-mysql  //查看该实例的历史版本
REVISION    UPDATED                     STATUS      CHART       DESCRIPTION     
1           Tue Sep  1 13:26:01 2020    SUPERSEDED  mysql-0.3.5 Install complete
2           Tue Sep  1 13:37:45 2020    DEPLOYED    mysql-0.3.5 Upgrade complete

[root@master ~]# helm rollback test-mysql 1  //将该实例回滚到版本1
[root@master ~]# helm list   //再次查看,可以看到“REVISION”的值变成了3
NAME        REVISION    UPDATED                     STATUS      CHART       APP VERSION NAMESPACE
test-mysql  3           Tue Sep  1 13:55:57 2020    DEPLOYED    mysql-0.3.5             default  

[root@master ~]# kubectl get pod | grep test-mysql  //确认pod是正常状态
test-mysql-mysql-dfb9b6944-fd6l9         1/1     Running   0          67s

[root@master ~]# kubectl  describe  pod test-mysql-mysql-dfb9b6944-fd6l9 
//查看pod的详细信息,可以看出,该pod使用的镜像已经回滚到mysql:5.7.14版本
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  2m4s  default-scheduler  Successfully assigned default/test-mysql-mysql-dfb9b6944-fd6l9 to node02
  Normal  Pulled     2m3s  kubelet, node02    Container image "busybox:1.25.0" already present on machine
  Normal  Created    2m3s  kubelet, node02    Created container remove-lost-found
  Normal  Started    2m3s  kubelet, node02    Started container remove-lost-found
  Normal  Pulled     2m1s  kubelet, node02    Container image "mysql:5.7.14" already present on machine
  Normal  Created    2m1s  kubelet, node02    Created container test-mysql-mysql
  Normal  Started    2m1s  kubelet, node02    Started container test-mysql-mysql

四、部署helm私有仓库

1)node01节点运行web容器作为私有仓库

[root@node01 ~]# mkdir -p /var/www/charts //创建目录,用于存放charts包
[root@node01 ~]# docker run -d -p 8080:80 -v /var/www:/usr/local/apache2/htdocs httpd

2)master节点上,创建chart包并打包

[root@master ~]# helm create testchart  //创建chart包(生成的是目录),名称为testchart
[root@master ~]# helm package testchart 
//将刚才生成的目录进行打包,会在当前目录下生成名为testchart-0.1.0.tgz的包

3)master节点生成仓库的index文件

[root@master ~]# mkdir myrepo
[root@master ~]# mv testchart-0.1.0.tgz myrepo/
//创建用于存放chart包的目录,并将chart包移动到该目录

[root@master ~]# helm repo index myrepo/ --url http://192.168.45.141:8080/charts
//url路径表示运行web容器的节点IP+端口,也就是指定的是node01节点
[root@master ~]# ls myrepo/
index.yaml  testchart-0.1.0.tgz
//确认已经生成index.yaml文件

[root@master ~]# scp myrepo/* node01:/var/www/charts
//将生成的index.yaml文件及charts包复制到web容器所在节点映射的本地目录

4)将新repo仓库添加到helm

[root@master ~]#  helm repo add newrepo http://192.168.45.141:8080/charts
//url是web容器所在的节点的IP+端口+存放chart包的目录,并指定仓库的名称为newrepo

[root@master ~]# helm repo list 
NAME    URL                                                   
stable  https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
local   http://127.0.0.1:8879/charts                          
newrepo http://192.168.45.141:8080/charts  
//stable是刚创建helm添加的阿里云的仓库
//local是本地的缓存
//newrepo是刚刚添加的仓库名称(确认该仓库已经存在)

注:配置至此,已经可以正常供内网环境使用这个charts包的私有仓库了,下面是一些验证。
5)搜索可用的chart包并部署

[root@master ~]# helm search testchart
NAME                CHART VERSION   APP VERSION DESCRIPTION                
local/testchart     0.1.0           1.0         A Helm chart for Kubernetes
newrepo/testchart   0.1.0           1.0         A Helm chart for Kubernetes
//local是本地的缓存,不用理会,可以查看到newrepo提供的testchart包

[root@master ~]# helm install newrepo/testchart -n test-nginx
//既然可以搜索到就表示可以使用chart包创建实例
[root@master ~]# kubectl  get  pod | grep test-nginx  //确认pod运行成功
test-nginx-testchart-64d8585997-jxtv7    1/1     Running   0          33s
[root@master ~]# kubectl describe  pod test-nginx-testchart-64d8585997-jxtv7 
//查看这个pod的详细信息,并确认pod所使用的镜像
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  2m28s  default-scheduler  Successfully assigned default/test-nginx-testchart-64d8585997-jxtv7 to node01
  Normal  Pulling    2m28s  kubelet, node01    Pulling image "nginx:stable"
  Normal  Pulled     2m10s  kubelet, node01    Successfully pulled image "nginx:stable"
  Normal  Created    2m10s  kubelet, node01    Created container testchart
  Normal  Started    2m9s   kubelet, node01    Started container testchart

6)更新chart包中所运行服务的镜像
其实就是一个服务版本升级的操作,大概思路如下:修改解压后的charts包目录下的values.yaml文件为所需的值(values.yaml文件包含的是这个服务的值,可以在里面指定镜像及标签、数据持久化的类型等等),修改完成后,再使用下面的命令进行升级操作。

[root@master ~]# helm upgrade -f testchart/values.yaml test-nginx newrepo/testchart
//my-nginx是之前实例的名称,可以通过“helm list”查看实例名称
//newrepo/testchart :为自定义仓库中的chart包

[root@master ~]# kubectl describe pod test-nginx-testchart-6d6984ddd9-gn9sw
//确认pod所使用的镜像已经更新

注:如果使用命令的方式直接进行升级,命令执行完成不会报错,但是镜像并不会更新,推荐使用更改values.yaml的方式进行升级!

7)再次向newrepo仓库上传chart包

[root@master ~]# helm create mychart
[root@master ~]# helm package mychart/
[root@master ~]# mv mychart-0.1.0.tgz myrepo/
[root@master ~]# helm repo index myrepo/ --url http://192.168.45.141:8080/charts
//当有新的chart包时,需更新index.yaml文件
[root@master ~]# scp myrepo/* node01:/var/www/charts
[root@master ~]# helm repo update
//如果需要使用则需更新一下本地的仓库

[root@master ~]# helm search mychart
NAME            CHART VERSION   APP VERSION DESCRIPTION                
local/mychart   0.1.0           1.0         A Helm chart for Kubernetes
newrepo/mychart 0.1.0           1.0         A Helm chart for Kubernetes
//确保新添加的chart包可以被搜索到

五、Helm常用命令

[root@master ~]# helm version        //查看helm版本信息
[root@master ~]# helm list               /查看当前安装的Chart包
[root@master ~]# helm search mysql         //查看与mysql相关的chart包
[root@master ~]# helm fetch stable/mysql            //将mysql软件包下载到本地
[root@master ~]# helm inspect stable/mysql       //查看该软件包的详细信息
[root@master ~]# helm install stable/mysql -n mysql        //安装指定的mysql软件包,并命名为mysql
[root@master ~]# helm status mysql                 //查看mysql的状态信息
[root@master ~]# helm delete --purge mysql       //删除mysql,并将本地的缓存也进行删除
[root@master ~]# helm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
//添加阿里云的repo源
[root@master ~]# helm repo update              //更新repo仓库资源
[root@master ~]# helm create helm_charts       //创建一个chart,名称为helm_charts
[root@master ~]# cd helm_charts/ && helm lint       //测试charts语法
[root@master ~]# helm package helm_charts           //打包charts
[root@master helm_charts]# helm template helm_charts-0.1.0.tgz       //查看该软件包生成的yaml文件

K8s之Helm工具详解

原文:https://blog.51cto.com/14306186/2526702

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!