ROS根据域名选择解析的DNS服务器

/ip firewall layer7-protocol
add name=testdns regexp=xiaocaicai.com

/ip firewall nat
add action=dst-nat chain=dstnat disabled=no  dst-port=53 layer7-protocol=testdns protocol=udp to-addresses=8.8.8.8 to-ports=53
add action=dst-nat chain=dstnat disabled=no  dst-port=53 layer7-protocol=testdns protocol=tcp to-addresses=8.8.8.8 to-ports=53
  1. Change the regex to match your domain.
  2. Change 8.8.8.8 to be your AD DNS server

参考文章

ZUUL modify request query param

对于url路由的服务来说,可以修改context的queryParam来达到修改请求参数的目的。

package com.efivestar.springcloud.gateway.filters.demo;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.util.List;
import java.util.Map;

import static com.netflix.zuul.context.RequestContext.getCurrentContext;

/**
 * @author Spencer Gibb
 */
public class QueryParamUpdatePreFilter extends ZuulFilter {
    private final Logger log = LoggerFactory.getLogger(getClass());

    public int filterOrder() {
        // run before PreDecorationFilter
        return 5 - 1;
    }

    public String filterType() {
        return "pre";
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = getCurrentContext();
        return context.getRequest().getRequestURI().startsWith("/test/");
    }

    public Object run() {
        RequestContext ctx = getCurrentContext();
        final Map<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();
        if(requestQueryParams.containsKey("access_token")) {
            requestQueryParams.put("accesstoken", requestQueryParams.get("access_token"));
            requestQueryParams.remove("access_token");
            ctx.setRequestQueryParams(requestQueryParams);
        }
        return null;
    }
}

参考资料

Docker 容器里无法使用 JDK 的 jmap 等命令的问题

线上java程序出现异常,需要打印内存信息进行debug,发现没有 jmap,jstack等工具。
发现容器基础镜像选择的是FROM java:8-jre,jre环境是不包含这类工具的,遂将换成FROM java:8,这类工具便包含在内了。
使用时发现还是不能使用,出现错误 “Can’t attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted docker”

查询资料后发现:
这其实不是什么 Bug,而是 Docker 自 1.10 版本开始加入的安全特性。
类似于 jmap 这些 JDK 工具依赖于 Linux 的 PTRACE_ATTACH,而是 Docker 自 1.10 在默认的 seccomp 配置文件中禁用了 ptrace。
相关资料

主要方法2个:
1.使用 –cap-add 明确添加指定功能:
docker run –cap-add=SYS_PTRACE …

2.Docker Compose 自 version 1.1.0 (2015-02-25) 起支持 cap_add。

version: '2'

services:
  mysql:
    ...
  api:
    ...
    cap_add:
      - SYS_PTRACE

ELK x-package watcher webhook配置

我们在配置watcher的时候,有时候期望将事件触发给自己写的服务来进行进一步的处理,这时候就需要写一些webhook了。

这里写个配置的例子。

{
"actions": {
    "test_webhook": {
      "webhook": {
        "scheme": "http",
        "host": "10.255.255.101",
        "port": 3000,
        "method": "post",
        "path": "/",
        "params": {},
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "{\"text\":{{#toJson}}ctx.payload{{/toJson}},\"channel\":\"123123@chatroom\",\"username\":\"{{ctx.metadata.name}}\"}",
        "connection_timeout_in_millis": 5000,
        "read_timeout_millis": 5000
      }
    }
  }
}

参考文档

ELK x-package watcher 正文JSON配置

x-package的监控还是很强大的,但是对于告警内容的二次过滤和开发能力较弱。某些情况下,我们可能需要将数据拿出来,和我们自己开发的数据处理平台对接,对数据进行清洗之后发送告警信息给管理员。

默认从ctx.payload拿到的数据是一种很奇怪的格式,我们可以通过toJson将数据转换成标准JSON格式进行解析。

{
  "trigger": {
    "schedule": {
      "interval": "1m"
    }
  },
  "input": {
    "search": {
      "request": {
        "search_type": "query_then_fetch",
        "indices": [
          "5ag-access-info*"
        ],
        "types": [],
        "body": {
          "query": {
            "bool": {
              "must": {
                "match": {
                  "statusCode": 500
                }
              },
              "must_not": [
                {
                  "match": {
                    "requestURI": "/prst"
                  }
                },
                {
                  "match": {
                    "requestURI": "/eep/api/weixin/getAcode"
                  }
                },
                {
                  "match": {
                    "requestURI": "/xsh/es-service/skuProduct/extendedWarrantyList"
                  }
                }
              ],
              "filter": {
                "range": {
                  "@timestamp": {
                    "from": "{{ctx.trigger.scheduled_time}}||-10000m",
                    "to": "{{ctx.trigger.triggered_time}}"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "condition": {
    "compare": {
      "ctx.payload.hits.total": {
        "gt": 0
      }
    }
  },
  "actions": {
    "notify-slack": {
      "slack": {
        "message": {
          "to": [
            "9581033544@chatroom"
          ],
          "text": "{{#toJson}}ctx.payload{{/toJson}}"
        }
      }
    }
  }
}

参考资料

Docker Swarm + CEPH 搭建过程

准备工作

硬件准备

我准备了3台虚拟化机器, 安装的是centos7系统(CentOS-7-x86_64-Everything-1708.iso)的最精简安装。

网络规划

服务器的 IP地址规划为:
swarm1:10.255.255.61
swarm2:10.255.255.62
swarm3:10.255.255.63

主机名和IP地址的映射我在路由器上配置好的,如果你没有配置dns,最好在本地hosts文件上配置好,以免发生意想不到的状况。

搭建目标

我的搭建目标是搭建一套docker swarm平台, 网络用 swarm的 overlay网络,存储使用ceph。

一些说明

由于之前没用接触过ceph存储,在百度 google一番之后,搜到很多 使用docker搭建ceph的教程(可能我搜索关键词有毛病。。汗)。
然后我就顺着教程进行搭建,然后其过程痛苦不堪,搞了2天,反正最终是没搞起来。
将要放弃之际,看了看ceph的官方文档(http://docs.ceph.org.cn/start/) ,发现居然有ceph-deploy这么神奇的东西。
然后就顺着教程搭建了一下,第一把搭建是比较坑的。后来查了下网络,有人说需要配置网络代理,有些包不用代理下不下来的,
然后我配置了下代理,顺利多了,根据教程就能很容易的搭建起来。代理配置

vim /etc/environment

在里面输入
http_proxy="http://username:password@proxy_server:port"

然后在命令行上
export http_proxy="http://username:password@proxy_server:port"

每个节点都需要配置

CEPH搭建

主要参考CEPH官方教程

预检

http://docs.ceph.org.cn/start/quick-start-preflight/

安装 CEPH 部署工具

由于我们用的centos7系统,使用RPM包的形式安装。

sudo yum install -y yum-utils && sudo yum-config-manager --add-repo https://dl.fedoraproject.org/pub/epel/7/x86_64/ && sudo yum install --nogpgcheck -y epel-release && sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 && sudo rm /etc/yum.repos.d/dl.fedoraproject.org*

把软件包源加入软件仓库。用文本编辑器创建一个 YUM (Yellowdog Updater, Modified) 库文件,其路径为 /etc/yum.repos.d/ceph.repo

vim /etc/yum.repos.d/ceph.repo

[Ceph-noarch]
name=Ceph noarch packages
baseurl=http://download.ceph.com/rpm-jewel/el7/noarch
enabled=1
gpgcheck=1
type=rpm-md
gpgkey=https://download.ceph.com/keys/release.asc
priority=1

更新软件库并安装 ceph-deploy

sudo yum update && sudo yum install ceph-deploy

NPT&用户

官方教程建议安装NTP(http://docs.ceph.org.cn/start/quick-start-preflight/#ntp) ,防止主机时间问题导致毛病。我检查下我的主机时间,发现没毛病,偷懒就没配置。
官方教程建议使用非root用户来安装,需建用户(http://docs.ceph.org.cn/start/quick-start-preflight/#ntp) 我偷懒直接用了root用户,就没进行配置。

允许无密码 SSH 登录

正因为 ceph-deploy 不支持输入密码,你必须在管理节点上生成 SSH 密钥并把其公钥分发到各 Ceph 节点。 ceph-deploy 会尝试给初始 monitors 生成 SSH 密钥对。
生成 SSH 密钥对,但不要用 sudo 或 root 用户。提示 “Enter passphrase” 时,直接回车,口令即为空:

ssh-keygen

Generating public/private key pair.
Enter file in which to save the key (/ceph-admin/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /ceph-admin/.ssh/id_rsa.
Your public key has been saved in /ceph-admin/.ssh/id_rsa.pub.

把公钥拷贝到各 Ceph 节点,把下列命令中的 {username} 替换成前面创建部署 Ceph 的用户里的用户名。

ssh-copy-id root@swarm1
ssh-copy-id root@swarm2
ssh-copy-id root@swarm3

开放所需端口

Ceph Monitors 之间默认使用 6789 端口通信, OSD 之间默认用 6800:7300 这个范围内的端口通信。详情见网络配置参考。 Ceph OSD 能利用多个网络连接进行与客户端、monitors、其他 OSD 间的复制和心跳的通信。
某些发行版(如 RHEL )的默认防火墙配置非常严格,你可能需要调整防火墙,允许相应的入站请求,这样客户端才能与 Ceph 节点上的守护进程通信。
对于 RHEL 7 上的 firewalld ,要对公共域开放 Ceph Monitors 使用的 6789 端口和 OSD 使用的 6800:7300 端口范围,并且要配置为永久规则,这样重启后规则仍有效。例如:
三台机器都必须要配置。

sudo firewall-cmd --zone=public --add-port=6789/tcp --permanent
sudo firewall-cmd --zone=public --add-port=6800-7300/tcp --permanent
sudo firewall-cmd --reload

SELINUX

在 CentOS 和 RHEL 上, SELinux 默认为 Enforcing 开启状态。为简化安装,我们建议把 SELinux 设置为 Permissive 或者完全禁用,也就是在加固系统配置前先确保集群的安装、配置没问题。用下列命令把 SELinux 设置为 Permissive :

sudo setenforce 0

永久关闭 vim /etc/selinux/config

SELINUX=disabled

这是我的准备工作配置,如果你在配置过程中遇到问题,最好还是看看官方的文档

存储集群快速安装

参考的文档

创建配置目录

我们创建一个 Ceph 存储集群,它有一个 Monitor 和两个 OSD 守护进程。一旦集群达到 active + clean 状态,再扩展它:增加第三个 OSD 、增加元数据服务器和两个 Ceph Monitors。为获得最佳体验,先在管理节点上创建一个目录,用于保存 ceph-deploy 生成的配置文件和密钥对。

mkdir /opt/my-cluster
cd /opt/my-cluster

ceph-deploy 会把文件输出到当前目录,所以请确保在此目录下执行 ceph-deploy

创建集群

如果在某些地方碰到麻烦,想从头再来,可以用下列命令清除配置:

ceph-deploy purge {ceph-node} [{ceph-node}]
ceph-deploy forgetkeys

在管理节点上,进入刚创建的放置配置文件的目录,用 ceph-deploy 执行如下步骤。

ceph-deploy new swarm1

把 Ceph 配置文件里的默认副本数从 3 改成 2 ,这样只有两个 OSD 也可以达到 active + clean 状态。把下面这行加入 [global] 段:

osd pool default size = 2

安装 Ceph

ceph-deploy install swarm1 swarm2 swarm3

配置初始 monitor(s)、并收集所有密钥:

ceph-deploy mon create-initial

完成上述操作后,当前目录里应该会出现这些密钥环:
{cluster-name}.client.admin.keyring
{cluster-name}.bootstrap-osd.keyring
{cluster-name}.bootstrap-mds.keyring
{cluster-name}.bootstrap-rgw.keyring

添加两个 OSD 。为了快速地安装,这篇快速入门把目录而非整个硬盘用于 OSD 守护进程。如何为 OSD 及其日志使用独立硬盘或分区,请参考 ceph-deploy osd 。登录到 Ceph 节点、并给 OSD 守护进程创建一个目录。

ssh swarm2
sudo mkdir /var/local/osd2
exit

ssh swarm3
sudo mkdir /var/local/osd3
exit

然后,从管理节点执行 ceph-deploy 来准备 OSD 。

ceph-deploy osd prepare swarm2:/var/local/osd2 swarm3:/var/local/osd3

最后,激活 OSD 。

ceph-deploy osd activate swam2:/var/local/osd2 swarm3:/var/local/osd3

用 ceph-deploy 把配置文件和 admin 密钥拷贝到管理节点和 Ceph 节点,这样你每次执行 Ceph 命令行时就无需指定 monitor 地址和 ceph.client.admin.keyring 了。

ceph-deploy admin swarm1 swarm2 swarm3

确保你对 ceph.client.admin.keyring 有正确的操作权限。

chmod +r /etc/ceph/ceph.client.admin.keyring

检查集群的健康状况。

ceph health

至此,CEPH集群搭建完毕,如果一切OK的话,最好给系统打个快照。

Docker Swarm集群搭建

docker安装

在3台机器上执行安装操作(参考地址

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine

sudo yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2

sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce

sudo systemctl start docker
sudo systemctl enable docker

docker就安装好了。

docker swarm初始化

参考教程 参考教程2
先把网络配置好
You need the following ports open to traffic to and from each Docker host participating on an overlay network:
* TCP port for cluster management communications
* TCP and UDP port 7946 for communication among nodes
* UDP port 4789 for overlay network traffic

firewall-cmd --add-port=2377/tcp --zone=public --permanent
firewall-cmd --add-port=7946/tcp --zone=public --permanent
firewall-cmd --add-port=7946/udp --zone=public --permanent
firewall-cmd --add-port=4789/udp --zone=public --permanent
firewall-cmd --reload

在swarm1节点上执行

docker swarm init --advertise-addr 10.255.255.61

docker swarm join --token SWMTKN-1-3i2e61fn9hgue070k2qnmsd7mcaapc9jwa2r1wyuao6rlyytr9-7wim3etfq5imqsywklutstacx 10.255.255.61:2377

他会给你一个其他节点加入集群的命令, 然后在其他节点上执行这条命令就可以把 swarm2 swarm3 加入到集群了。
这里有个超级巨坑要注意下, swarm初始化完了会创建一个ingress网络,我当时配置好,启动服务之后外部怎么都访问不了,郁闷死我了。后来查了半天,原来是ingress的网段和我服务器的网段冲突了。
我主机的默认网络是 10.255.255.0/24的, 这个ingress的默认网络是10.255.0.0/16的,是冲突的,真是晕死。
如果你在实践的时候,最好检查下网络,如果发现冲突,或者想重新规划,只需要删除这个网络,再重新创建就行了。参考这里

docker network ls

找到冲突的网络id

docker network rm network-id

docker network create \
  --driver overlay \
  --ingress \
  --subnet=10.200.0.0/24 \
  --gateway=10.200.0.2 \
  ingress

初始化一个你自己的服务

docker service create --name=nginx --publish 80:80  nginx

我在初始化集群的时候还遇到个服务不能转发的问题,搞了半天,也很郁闷, 然后我创建一个简单docker应用,暴露端口,然后docker提示我 网络转发没有开,服务可能不能正常访问。
如果你也遇到这个问题,尝试这样操作:参考这里

vim /etc/sysctl.conf
net.ipv4.ip_forward=1

systemctl restart network
sysctl -p

至此你的docker swarm集群应该初始化完毕了。

Portainer安装

portainer是一个用来管理docker swarm集群的可视化工具,非常好用。
先创建个网络

docker network create --driver overlay portainer_agent_network

配置portainer代理,这是全局的,所有节点上都会运行这个代理

docker service create \
    --name portainer_agent \
    --network portainer_agent_network \
    -e AGENT_CLUSTER_ADDR=tasks.portainer_agent \
    --mode global \
    --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \
    portainer/agent

创建一个portainer持久化目录

mkdir /opt/portainer_data

创建portainer服务

docker service create \
    --name portainer \
    --network portainer_agent_network \
    --publish 9000:9000 \
    --replicas=1 \
    --constraint 'node.role == manager' \
    --mount type=bind,source=/opt/portainer_data,destination=/data \
    portainer/portainer:latest -H "tcp://tasks.portainer_agent:9001" --tlsskipverify

你可以通过访问任意节点的 9000端口来访问portainer,初始化个密码就可以进行管理了。

rexray/rbd安装

ceph装好了,docker swarm也装好了,如何来使用呢。
你需要装个docker插件 具体参考

docker plugin install rexray/rbd RBD_DEFAULTPOOL=rbd

装好之后,你就可以创建你的存储卷了

docker volume create --driver rexray/rbd:latest cephvol1

你也可以在portainer界面上进行创建。
Requirements

The RBD plug-in requires that the host has a fully working environment for mapping Ceph RBDs, including having the RBD kernel module already loaded. The cluster configuration and authentication files must be present in /etc/ceph.

至此,全部完成了。后面的我还没实践,等实践了再来更新。

——————–分割线—————-

我又来了, 上面其实还没搞完。

在上面的基础上,我发现可以创建 基于rexray/rbd的存储卷, 然后在我尝试把存储挂到我容器上面的时候,容器怎么都启动不了。
搞了半天,看系统日志有报错,看到 docker 有尝试 和 “unix:///var/run/rexray/998287049.sock” 联系,但是联系不上。
查了些资料,思考了下,感觉 应该是这样玩的。
我前面装了个 rexray/rbd 只是个 rexray给rbd存储写的插件, 我们还需要装个rexray才可以,看了rexray官网文档,也确实是这样。

那么开干吧。参考文档 (IBM) (REXRAY

安装 rexray

curl -sSL https://dl.bintray.com/emccode/rexray/install | sh -

在每个节点上面都要安装。

然后启动rexray

rexray start

也可以debug模式启动
REXRAY_DEBUG=true rexray start

然后再尝试 创建卷,挂载卷, 好像可以玩了。 噢耶。

配置rexray为服务

Install as a service with

rexray install

This will register itself with SystemD or SystemV for proper initialization.

Once configured the REX-Ray service can be started with the command ‘sudo systemctl start rexray’.

增加rexray的配置

配置生成器

Rexray 官方文档

再次更新,关于 权限问题。
在我尝试给NGINX挂载一个卷之后,访问发现nginx返回的是403,看了下日志,报的是 permission deny,没有读文件权限。
查看了文件权限之后 是 700, root的权限,nginx没权限读取文件的。
然后研究了很多文章,查了很多文档。然后目前可以通过这样配置来修改权限。

docker plugin install rexray/rbd RBD_DEFAULTPOOL=rbd LINUX_VOLUME_FILEMODE=0777

然后我把插件卸载了,重新安装了下,目前看来权限是OK的,可以控制。 参考文档
参考文档

portainer的二次开发

一、准备环境
依赖:Docker, Node.js >= 0.8.4 和 npm

[root@dev_08 ~]# curl --silent --location https://rpm.nodesource.com/setup_7.x | sudo bash -
[root@dev_08 ~]# yum install -y nodejs
[root@dev_08 ~]# npm install -g grunt-cli

二、构建
1、checkout
[root@dev_08 ~]# cd /opt
先 fork 一个 portainer的分支,然后 clone 到本地, 然后在 branch 上开发,例如:
[root@dev_08 opt]# git clone https://github.com/opera443399/portainer.git
[root@dev_08 opt]# cd portainer
[root@dev_08 portainer]# git checkout -b feat-add-container-console-on-task-details
Switched to a new branch 'feat-add-container-console-on-task-details'
[root@dev_08 portainer]# git branch
  develop
* feat-add-container-console-on-task-details


2、使用 npm 安装依赖包
[root@dev_08 portainer]# npm install -g bower && npm install

3、根目录没有这个目录: bower_components 的话则执行
[root@dev_08 portainer]# bower install --allow-root


4、针对 centos 执行
[root@dev_08 portainer]# ln -s /usr/bin/sha1sum /usr/bin/shasum


5、构建 app
[root@dev_08 portainer]# grunt build

如果遇到这样的错误:
Building portainer for linux-amd64
/go/src/github.com/portainer/portainer/crypto/crypto.go:4:2: cannot find package "golang.org/x/crypto/bcrypt" in any of:
    /usr/local/go/src/golang.org/x/crypto/bcrypt (from $GOROOT)
    /go/src/golang.org/x/crypto/bcrypt (from $GOPATH)
/go/src/github.com/portainer/portainer/http/handler/websocket.go:21:2: cannot find package "golang.org/x/net/websocket" in any of:
    /usr/local/go/src/golang.org/x/net/websocket (from $GOROOT)
    /go/src/golang.org/x/net/websocket (from $GOPATH)
mv: cannot stat ‘api/cmd/portainer/portainer-linux-amd64’: No such file or directory
Warning: Command failed: build/build_in_container.sh linux amd64
mv: cannot stat ‘api/cmd/portainer/portainer-linux-amd64’: No such file or directory
 Use --force to continue.

Aborted due to warnings.

那是因为网络可达性问题,国内访问 golang.org 异常。
[root@dev_08 portainer]# host golang.org
golang.org is an alias for golang-consa.l.google.com.
golang-consa.l.google.com has address 216.239.37.1


导致这2个依赖下载失败:
golang.org/x/crypto/bcrypt
golang.org/x/net/websocket


解决方法:
[root@dev_08 portainer]# go get github.com/golang/crypto/tree/master/bcrypt
[root@dev_08 portainer]# go get github.com/golang/net/tree/master/websocket

[root@dev_08 portainer]# cd $GOPATH/src
[root@dev_08 src]# mkdir golang.org/x -p
[root@dev_08 src]# mv github.com/golang/* golang.org/x/


然后再切换到源码目录,调整构建脚本: 
[root@dev_08 src]# cd /opt/portainer
[root@dev_08 portainer]# vim build/build_in_container.sh
挂载本地的 $GOPATH/src/golang.org 到容器路径:/go/src/golang.org

docker run --rm -tv $(pwd)/api:/src -e BUILD_GOOS="$1" -e BUILD_GOARCH="$2" portainer/golang-builder:cross-platform /src/cmd/portainer

调整为:

docker run --rm -tv $(pwd)/api:/src -v $GOPATH/src/golang.org:/go/src/golang.org -e BUILD_GOOS="$1" -e BUILD_GOARCH="$2" portainer/golang-builder:cross-platform /src/cmd/portainer


最后重新构建一次:
[root@dev_08 portainer]# grunt build
(略)
Cleaning "dist/js/angular.37dfac18.js"...OK
Cleaning "dist/js/portainer.cab56db9.js"...OK
Cleaning "dist/js/vendor.4edc9b0f.js"...OK
Cleaning "dist/css/portainer.e7f7fdaa.css"...OK

Done, without errors.


看到上述输出,表示符合预期。




6、运行(可以自动重启)
[root@dev_08 portainer]# grunt run-dev

访问 UI 地址: http://localhost:9000



7、不要忘记 lint 代码
[root@dev_08 portainer]# grunt lint



8、release(通常我们使用 linux-amd64 这个平台,具体过程请参考脚本 build.sh)
[root@dev_08 portainer]# grunt "release:linux:amd64"
(略)
Done, without errors.
[root@dev_08 portainer]# ls dist/
css  fonts  ico  images  index.html  js  portainer-linux-amd64
[root@dev_08 portainer]# mv dist/portainer-linux-amd64 dist/portainer



9、打包成镜像
[root@dev_08 portainer]# docker build -t 'opera443399/portainer:dev' -f build/linux/Dockerfile .



10、测试上述镜像
[root@dev_08 portainer]# mkdir -p /data/portainer_dev
[root@dev_08 portainer]# docker run -d -p 9001:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /data/portainer_dev:/data --name portainer_dev opera443399/portainer:dev
[root@dev_08 portainer]# docker ps -l
CONTAINER ID        IMAGE                                   COMMAND             CREATED             STATUS              PORTS                    NAMES
cbd986df765b        opera443399/portainer:dev               "/portainer"        8 seconds ago       Up 7 seconds        0.0.0.0:9001->9000/tcp   portainer_dev

首次使用时将初始化一个管理员账户(本例使用 httpie 来提交)
[root@dev_08 portainer]# http POST :9001/api/users/admin/init Username="admin" Password="Develop"
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Date: Tue, 10 Oct 2017 08:18:19 GMT
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

访问页面:your_dev_ip:9001
验证功能:符合预期

清理:
[root@dev_08 portainer]# docker rm -f portainer_dev




三、开发需求示例
可结合 github 的搜索功能来查找关键字。

1、需求:在 'Container list' 页面,设定初始化时,默认不显示所有容器(即默认不勾选 'Show all containers' 这个复选框)
注1:从 1.14.1 版本开始使用 cookie 来记录是否显示所有的状态(Persist the status of the show all containers filter: #1198),其实完全可以不更改代码,去掉 checkbox 的选择后,下次登录还是 unchecked 的状态,本例仅作为修改代码的一个 howto 来展示。
注2:从 1.14.1 版本开始,新增了针对资源的限制(Add the ability to manage CPU/MEM limits & reservations for Swarm services: #516),不妨一试。
https://github.com/portainer/portainer/releases



为了达到我们的小目标,需要调整 filter_containerShowAll 在初始化时的默认值为 false 即可。

[root@dev_08 portainer]# diff -u /tmp/localStorage.js app/services/localStorage.js
--- /tmp/localStorage.js   2017-09-26 15:11:41.062167776 +0800
+++ app/services/localStorage.js   2017-09-26 15:21:11.604992708 +0800
@@ -50,7 +50,7 @@
     getFilterContainerShowAll: function() {
       var filter = localStorageService.cookie.get('filter_containerShowAll');
       if (filter === null) {
-        filter = true;
+        filter = false;
       }
       return filter;
     }



2、需求:在 'Service details' 页面的 'Tasks' 标签页中增加一个过滤器
当前操作列出了所有的 tasks
而我有时候只需要查看处于 running 状态的 tasks 即可,因而需要一个过滤器,改动代码如下:

[root@dev_08 portainer]# diff -u /tmp/tasks.html app/components/service/includes/tasks.html
--- /tmp/tasks.html    2017-09-26 18:00:45.893169748 +0800
+++ app/components/service/includes/tasks.html 2017-09-26 18:22:10.188208664 +0800
@@ -12,6 +12,11 @@
       </select>
       </div>
     </rd-widget-header>
+    <rd-widget-taskbar classes="col-lg-12">
+      <div class="pull-right">
+        <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
+      </div>
+    </rd-widget-taskbar>
     <rd-widget-body classes="no-padding">
       <table class="table">
       <thead>
@@ -48,7 +53,7 @@
         </tr>
       </thead>
       <tbody>
-        <tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
+        <tr dir-paginate="task in (filteredTasks = ( tasks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
         <td><a ui-sref="task({ id: task.Id })" class="monospaced">{{ task.Id }}</a></td>
         <td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
         <td ng-if="service.Mode !== 'global'">{{ task.Slot }}</td>


具体请参考:
https://github.com/portainer/portainer/pull/1242



3、需求:在 'Service details > Tasks > Task details' 页面增加一个 Container Console 按钮
当前操作要找到 service 下所有容器的 Console 并打开,则要执行下述步骤:
1 个 service 对应 N 个 tasks(containers) (运行在 M nodes 中)

    -> 打开 'Service Details' 页面,过滤处于 running 状态的 tasks 并注意对应的 node 名称  
    -> 选择 node N1 并切换到 endpoint N1
    -> 在 N1 的 'Container List' 页面上通过过滤器找到 container C1 
    -> 在 'Container Details' 页面打开 Console


为了简化操作,实现咱们的小目标,改动代码如下:
    -> 在 'Service details > tasks' 页面,打开 'Task details' 页面时跳转到新的标签页
    -> 在 'Task details' 页面,新增 container console 按钮,新增一个提示框,提示后续步骤遇到错误时如何处理
    -> 点击 Console 按钮打开 'Container console' 页面时,跳转到新的标签页
            如果该 Container 不在当前 endpoint 中运行,则有一个错误通知:
                'Failure: Docker container identifier not found'
            此时,我们要做的就是一个 endpoint 切换的操作
    -> 当我们切换 endpoint 时,不是跳转页面到 dashboard 页面,而仅仅使用一个消息通知,然后等待页面刷新后,得到的页面即我们想要的。
    -> 如果我们切换到 swarm worker node 则后续需要一个切换回 swarm manager node 的操作
    -> 小结:用户体验有点糟糕,但能工作。




[root@dev_08 portainer]# diff -u /tmp/tasks.html app/components/service/includes/tasks.html
--- /tmp/tasks.html    2017-10-12 11:55:23.247181711 +0800
+++ app/components/service/includes/tasks.html 2017-10-12 10:12:52.653767466 +0800
@@ -54,7 +54,7 @@
       </thead>
       <tbody>
         <tr dir-paginate="task in (filteredTasks = ( tasks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
-        <td><a ui-sref="task({ id: task.Id })" class="monospaced">{{ task.Id }}</a></td>
+        <td><a ui-sref="task({ id: task.Id })" class="monospaced" target="_blank">{{ task.Id }}</a></td>
         <td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
         <td ng-if="service.Mode !== 'global'">{{ task.Slot }}</td>
         <td>{{ task.NodeId | tasknodename: nodes }}</td>



[root@dev_08 portainer]# diff -u /tmp/task.html app/components/task/task.html 
--- /tmp/task.html 2017-10-11 14:22:29.782178036 +0800
+++ app/components/task/task.html  2017-10-12 10:13:28.367767064 +0800
@@ -44,7 +44,12 @@
             </tr>
             <tr ng-if="task.Status.ContainerStatus.ContainerID">
               <td>Container ID</td>
-              <td>{{ task.Status.ContainerStatus.ContainerID }}</td>
+              <td title="Notify 'Failure: Docker container identifier not found' -> Try 'Switch endpoint to the node where the container is running'">
+                {{ task.Status.ContainerStatus.ContainerID }}
+                <a class="btn btn-outline-secondary" target="_blank" type="button" ui-sref="console({id: task.Status.ContainerStatus.ContainerID})">
+                  <i class="fa fa-terminal space-right" aria-hidden="true"></i>Console
+                </a>
+              </td>
             </tr>
           </tbody>
         </table>



[root@dev_08 portainer]# diff -u /tmp/sidebarController.js app/components/sidebar/sidebarController.js 
--- /tmp/sidebarController.js  2017-10-12 11:57:10.063180508 +0800
+++ app/components/sidebar/sidebarController.js    2017-10-12 09:37:32.314244435 +0800
@@ -14,7 +14,7 @@
     EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
     StateManager.updateEndpointState(true)
     .then(function success() {
-      $state.go('dashboard');
+      Notifications.success('switch endpoint to: ', endpoint.Name);
     })
     .catch(function error(err) {
       Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');


具体请参考:
https://github.com/portainer/portainer/pull/1271


四、源码分析示例
1、在 UI 界面中是如何请求 docker api 来完成 container list 之类的请求的?
分析过程:

1)例如,在 Container list 页面,有如下请求:
/api/endpoints/1/docker/containers/json?all=0

列出了处于 running 状态的容器,我们去找找相关的代码。


2)找找 js 代码
[root@dev_08 portainer]# find ./app -type f -name '*.js' -exec grep -l 'docker\/containers' {} \;
./app/rest/docker/container.js
./app/rest/docker/containerLogs.js
[root@dev_08 portainer]# 

[root@dev_08 portainer]# vim app/rest/docker/container.js

angular.module('portainer.rest')
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
  'use strict';
  return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', {
    name: '@name',
    endpointId: EndpointProvider.endpointID
  },



3)找找 go 代码
[root@dev_08 portainer]# find ./api -type f -name '*.go' -exec grep -l '\/api\/endpoints' {} \;
./api/http/handler/handler.go
[root@dev_08 portainer]# 


[root@dev_08 portainer]# vim api/http/handler/handler.go
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

        case strings.HasPrefix(r.URL.Path, "/api/endpoints"):                                   // 该请求的 URL 是 /api/endpoints 开头的
                if strings.Contains(r.URL.Path, "/docker") {                                    // 且URL 包含 /docker
                        http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)     // 去掉 /api/endpoints 变成 /1/docker/containers/json?all=0  然后交给 DockerHandler 来处理


[root@dev_08 portainer]# vim api/http/handler/docker.go
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {

        http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)                               // 去掉 /1/docker 变成 /containers/json?all=0  (这就是最终请求 docker api 的路径)


通过上述代理行为,最终变成请求类似这样的 docker api (具体的请求是走的本地 socket 还是 tcp 的行为尚未分析)

curl -s \
    --unix-socket /var/run/docker.sock \
    http:/containers/json?all=0 

或者:

curl -s \
    http://endpoint_ip:port/containers/json?all=0 




五、FAQ
1、为何不能统一管理 swarm mode 集群下的资源?
状态:
我理解,这个UI的开发理念是一个简单通用的docker UI
目前的方式是:每个 node 都是一个 endpoint,要切换 endpoint 来执行 docker API 指令获取该 node 上的容器等信息。
未来计划增加 swarm mode 集群管理的功能。

具体的讨论,还请跟踪下这个 issue 相关的内容:
[FEATURE REQUEST] Be able to use all the Portainer built-in functionalities in all the containers running in a swarm cluster #461
https://github.com/portainer/portainer/issues/461



2、如何提交 github PR 来为本项目共享代码?
请先阅读:https://github.com/portainer/portainer/blob/develop/CONTRIBUTING.md

贡献代码的指引
1)通用
先看看有没有相关的 PR/issues 已经存在,如果没有则进入下一步
打开一个 issue 讨论你要带来的一些变化(也可以在已经存在 的 issue 上讨论),包括功能合理性和技术可行性等方面,在解决方案上讨论出一个解决,一致同意后,再去开发【这一步很重要,不能跨过】
在一个特定主题的分支上开发,不要在 master/develop 分支开发!
新建分支时,有一些命名规则,例如:
(type)(issue number)-text-desc

举例:
你在提交一个 bugfix 来解决 issue #361 则分支可以命名为: fix361-template-selection.
参考这里的讨论:
https://github.com/portainer/portainer/issues/1271#issuecomment-336033282

2)为已经打开的 issues 贡献代码
想做却不知道如何开始?

有一些 issues 的标签是 exp/ 开头的,意味着可以供大家去开发,但有难度的区分:
beginner: 针对不太熟系该项目代码的开发者
intermediate: 针对该项目代码有一定理解,或对 AngularJS or Golang 有使用经验的开发者
advanced: 针对该项目代码有深入理解的开发者

可以使用 Github filters 来过滤出 issues:

beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced

3)代码 Linting

检查代码使用 grunt lint 然后再提交 PR


4)提交的信息格式
 <type>(<scope>): <subject>
不要超过 100 字符,整洁的 commit log 示例:

 #271 feat(containers): add exposed ports in the containers view
 #270 fix(templates): fix a display issue in the templates view
 #269 style(dashboard): update dashboard with new layout


有如下几类 type

feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
refactor: A code change that neither fixes a bug or adds a feature
test: Adding missing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
Scope

关于 scope :表示
提交的代码是改动了哪一块的内容,例如:networks, containers, images 可以使用 area 标签来关联 issue (例如:标签 area/containers 使用 containers 作为a scope)

关于 subject :表示
简明的描述本次提交做了那些改变

使用祈使句和现在时: "change" not "changed" nor "changes"
不要大些第一个字母
语句最后不要使用点 (.) 

NetCat的一些小技巧

功能超级强大的网络工具netcat

A机器执行 nc -l 1234
B机器执行 nc a-ip 1234
AB之间就可以联通了, 可以当聊天工具用。

A机器执行 nc -l 1234 -e /bin/bash -k
B机器执行 nc a-ip 1234
B机器就可以运行A机器的shell了, -k 相当于 –keep-open,保持连接 可以接多会话。

A机器执行 tailf /var/log/nginx/access.log | nc -l 1234 -k
B机器执行 nc a-ip 1234
B机器就可以实时看A机器的日志了。

A机器执行 nc -l 1234 < file.txt
B机器执行 nc a-ip 1234 > file.txt
B机器就可以下载到A机器的文件了。

使用Netcat在两台服务器之间传输文件

To send a directory, cd to inside the directory whose contents you want to send on the computer doing the sending and do:

tar -cz . | nc -q 10 -l -p 45454

On the computer receiving the contents, cd to where you want the contents to appear and do:

nc -w 10 $REMOTE_HOST 45454 | tar -xz

Replace $REMOTE_HOST with ip / hostname of computer doing the sending. You can also use a different port instead of 45454.

What’s actually happening here is that the ‘receiving’ computer is connecting to the sending computer on port 45454 and receiving the tar’d and gzip’d contents of the directory, and is passing that directly to tar (and gzip) to extract it into the current directory.

Quick example (using localhost as a remote host)

Computer 1

caspar@jumpy:~/nctest/a/mydir$ ls
file_a.txt file_b.log
caspar@jumpy:~/nctest/a/mydir$ tar -cz . | nc -q 10 -l -p 45454
Computer 2

caspar@jumpy:~/nctest/b$ ls
caspar@jumpy:~/nctest/b$ nc -w 10 localhost 45454 | tar -xz
caspar@jumpy:~/nctest/b$ ls
file_a.txt file_b.log