django database router是个好东西

今天给sentry加了MySQL读写分离机制,记录一下:

DATABASES里,(sentry的脾气比较怪)保留名为default的配置,写master数据库的参数;新增一个名为slave的配置,使用只读用户名密码,或开启服务器端read only
然后增加一个类,带四个函数:

class DatabaseRWSplitRouter(object):
    def db_for_read(self, model, **hints):
        return 'slave'

    def db_for_write(self, model, **hints):
        return 'default'

    def allow_relation(self, model1, model2, **hints):
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if db == 'slave':
            return None
        else:
            return True

再把这个注册进去

DATABASE_ROUTERS = (DatabaseRWSplitRouter(), )

注意这个注册,可以写字符串形式的dotted_path也可以是一个对象,我偷懒就直接写了一个对象。

Posted in 默认分类 | Tagged , , | Leave a comment

内训教程释出:from CGI to WSGI

古代时,读过MSDN里的《from C++ to COM》受益匪浅。后来学习Python,自己也写了一篇 from CGI to WSGI 的教程,用于内部培训。

http://julyclyde.org/pyCGIWSGI.pdf

Posted in 默认分类 | Tagged | Leave a comment

给rq添加了RedisCluster支持

最近一直在和前同事Puff潘、RainSun吕合作,做一些给使用redis的软件增加redis cluster或者redis sentinel支持功能的工作。

传统上,做sharding工作有三种做法:

  1. 客户端支持,比如redis cluster客户端需要支持KeySlot计算(我报告的bug: https://github.com/Grokzen/redis-py-cluster/issues/153)
  2. 代理层支持:twemproxy、codis等
  3. 服务器端支持:比如MySQL partition table之类

我比较推崇的是第二种,但redis的cluster和sentinel都属于第一种,客户端需要有明确的能力和知识去处理连接多个服务器的问题,而这方面,各常见客户端库做的并不好。另外还有个问题是用Python语言写的各应用软件,往往直接写死了用redis库,而不是用rediscluster库或者运行期动态决定,导致根本不具备使用cluster的能力。

近期在看rq的时候,发现rq支持custom worker class,于是我就改了改,让它也支持custom connection class,并说服了作者merge进来。

https://github.com/nvie/rq/pull/741

不过其实我觉得还是略别扭。主要是因为生成connection对象的时候套了两层函数,为了补全function signature,并尽量减少其它代码的修改,不得不在两层都使用参数默认值,显得多余。尽管如此,还是很为自己能为基础软件做出贡献而感到高兴的。

Posted in 默认分类 | Leave a comment

kube-proxy –proxy-mode=iptables 与 rp_filter 冲突

2015年12月16日,朱鹏安装了新版kubernetes master版本(比1.1新,为1.2alpha**),然后发现,访问 clusterIP:clusterPort 会发生无法连接的故障。

从集群内Node上访问

分别在Node rz-ep19和container里执行curl访问,发现container里curl可以成功访问,但Node上一般不行(偶尔成功,几率很低)。查看iptables规则,发现新版kube-proxy已经不再将请求REDIRECT到本机kube-proxy端口,而是:

  1. 先把PREROUTING、OUTPUT无条件指向KUBE-SERVICES链;如匹配不上KUBE-SERVICES链,则再尝试匹配发给docker0
  2. 在KUBE-SERVICES链里匹配clusterIP:clusterPort条件,然后发到KUBE-SVC-***链
  3. 在KUBE-SVC-***链中,用-m statistic –mode random –probability这样的条件将流量按等比例分给多个KUBE-SEP-***链
  4. 然后再在KUBE-SEP-***链中,将数据包DNAT给endpoint

为了减少干扰,我们缩减了kube-system/elasticsearch-logging 10.16.59.73:9200服务的规模,到只有一个endpoint 172.16.86.48:9200运行在rz-ep10上;rz-ep19 10.16.49.16作为运行curl的客户端

  • 在rz-ep19的flannel接口上抓包,抓到了 10.16.49.16->172.17.86.48的TCP SYN,但没有收到回应。
  • 在rz-ep10(endpoint Pod所在的Node)的flannel接口上抓包,抓到了和上述相同的包,也没有收到回应。
  • 在rz-ep10的docker0接口上抓包,没有抓到

由此判断,rz-ep10的内核在转发时主动丢弃掉了 10.16.49.16->172.17.86.48的SYN,以至于无法建立TCP连接。查看/proc/sys/net/ipv4/conf/{all,flannel.7890}/rp_filter,发现flannel.7890/rp_filter内容为1,即在此网卡上执行“根据回溯路由检查数据包是否为伪造”的检查。因为源IP 10.16.49.16在rz-ep10看来理应出现自eth0而非flannel.7890接口,所以被判定为假造包,丢弃。

将此参数改为0,再去docker0上抓包,可以收到172.17.86.48发回10.16.49.16 SYNACK包;但rz-ep19上curl仍显示无法建立连接。

在rz-ep10的角度考虑,这个172.17.86.48->10.16.49.16的包应该从eth0发出,也就是在rz-ep19的eth0上收到。而在rz-ep19的角度考虑,源IP 172.16.86.48不应来自eth0,也会被rp_filter参数影响,丢弃掉,所以无法建立连接。把rz-ep19的eth0/rp_filter参数改为0,终于可以正常访问了。

 

plantuml9098040596798137974

 

从集群外访问

从办公区我的笔记本电脑 172.30.26.169 访问 10.16.59.67:80 服务,该虚IP被手工绑在rz-ep01上,ping可以通,但访问不通。

在笔记本电脑上抓包,发现只有从本机发往clusterIP的SYN包,没有返回,所以无法建立TCP连接。

改以Pod IP为过滤条件,发现Pod IP直接发回 SYN_ACK给我的笔记本电脑,但因为笔记本电脑这边没有发起对Pod的SYN,所以直接回复RST给Pod了。

改用iptables模式之后,由于不对称路径的问题,这种访问基本上无法以以前“把clusterIP绑在Node上”的做法实现

Posted in 默认分类 | Tagged | Leave a comment

Kubernetes内的网络通信问题

首先复习一下Kubernetes内的对象类型

  1. Node:运行kubelet(古代叫minion)的计算机
  2. Pod:最小调度单位,包含一个pause容器、至少一个运行应用的容器
  3. RC:复本控制器,用于保持同类Pod的并行运行的数量
  4. Svc:暴露服务的可访问通信接口

 

对象之间的通信关系

客户端
服务器
访问方式
master kubelet Node的10250/TCP端口。Node报到后,Master得知Node的IP地址
kubelet

&

kube_proxy

apiserver master:8080/TCP HTTP

配置文件写明master地址

kubectl命令行 apiserver localhost:8080/TCP或命令行参数指定
other ALL apiserver ClusterIP_of_kubernetes:443/TCP HTTPS

上述IP和端口号通过环境变量通知到容器

需要出示身份信息

kubernetes是一个ClusterIP模式的Service。参见下面详述

pod pod 跨Node的多个pod相互通信,需要通过overlay network,下面详述
ALL service 三种模式,下面详述

 

overlay network

kubernetes不提供pod之间通信的功能,需要装额外的软件来配合。我选的是出自CoreOS的flannel软件:

flannel是专门为docker跨Host通信而设计的overlay network软件,从ETCd获取配置,提供对docker网络参数进行配置的脚本,使用UDP、VXLAN等多种协议承载流量。

经实验,flannel在办公云(新)上会导致kernel panic

flannel配置

在/etc/sysconfig/flanneld 配置文件中写好etcd的地址

用etcdctl mk /coreos.com/network/config 命令将下列配置写入etcd:

{
"Network": "172.17.0.0/16",
"SubnetLen": 24,
"Backend": {
    "Type": "vxlan",
     "VNI": 7890
 }
 }

Network代表flannel管理的总的网络范围;SubnetLen是其中每个节点的子网掩码长度;Backend规定了各节点之间交换数据的底层承载协议。

再各Node执行 systemctl start flanneld 启动服务。

 

flannel对docker作用的原理

flannel 并非“神奇地”对docker产生作用。

查看/usr/lib/systemd/system/flanneld.service配置文件可知,在flanneld启动成功之后,systemd还会去执行一次/usr/libexec/flannel/mk-docker-opts.sh脚本,生成/run/flannel/docker环境变量文件。

flannel的RPM中包含的 /usr/lib/systemd/system/docker.service.d/flannel.conf  ,作为docker服务的配置片段,引用了上述环境变量文件 /run/flannel/docker ,故 flannel 必须在 docker 之前启动,如果在docker已经运行的情况下启动flannel,则docker也必须重启才能生效。。通过 systemctl status docker 可以看到

# systemctl status docker
docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled)
  Drop-In: /usr/lib/systemd/system/docker.service.d
           └─flannel.conf
   Active: active (running) since 一 2015-12-14 15:53:06 CST; 19h ago
     Docs: http://docs.docker.com
 Main PID: 149365 (docker)
   CGroup: /system.slice/docker.service
           └─149365 /usr/bin/docker daemon --selinux-enabled -s btrfs --bip=172.17.33.1/24 --mtu=1450 --insecure-registry docker.sankuai.com

flannel作用于docker,最终表现为设置了其–bip 参数,也就是 docker0 设备的IP地址。如重启时失败,可以用ip addr命令删除docker0上的IP然后再重新启动docker服务。

另外,flannel会在各节点上生成一个名为flannel.7890的虚拟接口(其中7890就是上面配置文件里的VNI号码)其掩码为/16,但IP和docker0的IP地址相近。这种配置生成的路由表如下:

即:与172.17.33.0/24 通信,通过docker0;与172.17.0.0/16 通信(不含172.17.33.0/24),通过flannel.7890 发往overlay network

Docker和容器的网络

用ip link add vethX peer name vethY命令添加一对虚拟以太网接口。

veth和普通eth接口的区别在于:veth一次就要配置一对,这一对veth接口有背对背的隐含连接关系,可以通过tcpdump和ping来验证。

然后把其中一个网卡移到容器的namespace里,另一个加入docker0 bridge,即完成将容器连接到docker0 bridge的工作。

查看namespace内情况的方法,请参见https://gist.github.com/vishvananda/5834761

Kubernetes Pod的网络配置

一个Pod最少包含两个容器,其中一个叫pause,它什么都不敢,只“持有”IP地址。通过docker inspect可以看到:

[
{
    "Id": "2b36d6cb2f2d822473aa69cacb99d8c21e0d593a3e89df6c273eec1d706e5be8",
    "NetworkSettings": {
        "Bridge": "",
        "EndpointID": "2c1e17f3fd076816d651e62a4c9f072abcc1676402fb5eebe3a8f84548600db0",
        "Gateway": "172.17.33.1",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "HairpinMode": false,
        "IPAddress": "172.17.33.2",
        "IPPrefixLen": 24,
        "IPv6Gateway": "",
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "MacAddress": "02:42:ac:11:21:02",
        "NetworkID": "6a23154bd393b4c806259ffc15f5b719ca2cd891502a1f9854261ccb17011b07",
        "PortMapping": null,
        "Ports": {},
        "SandboxKey": "/var/run/docker/netns/2b36d6cb2f2d",
        "SecondaryIPAddresses": null,
        "SecondaryIPv6Addresses": null
    },
    "HostConfig": {
        "NetworkMode": "default",
        "IpcMode": "",
    }
]

而真正运行应用的那个容器,网络配置则是这样:

[
{
    "Id": "eba0d3c4816fd67b10bcf078fb93f68743f57fa7a8d32c731a6dc000db5fafe8",
    "NetworkSettings": {
        "Bridge": "",
        "EndpointID": "",
        "Gateway": "",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "HairpinMode": false,
        "IPAddress": "",
        "IPPrefixLen": 0,
        "IPv6Gateway": "",
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "MacAddress": "",
        "NetworkID": "",
        "PortMapping": null,
        "Ports": null,
        "SandboxKey": "",
        "SecondaryIPAddresses": null,
        "SecondaryIPv6Addresses": null
    },
    "HostConfig": {
        "NetworkMode": "container:2b36d6cb2f2d822473aa69cacb99d8c21e0d593a3e89df6c273eec1d706e5be8",
        "IpcMode": "container:2b36d6cb2f2d822473aa69cacb99d8c21e0d593a3e89df6c273eec1d706e5be8",
    }
]

注意对比NetworkMode和IpcMode的值,后者的NetworkMode和IpcMode的值为前者的Id。

Kubernetes这种设计,是为了实现单个Pod里的多个容器共享同一个IP的目的。除了IP以外,Volume也是在Pod粒度由多个容器共用的。

Kube-Proxy服务

kubernetes各节点的kube-proxy服务启动后,会从apiserver拉回数据,然后设置所在机器的iptables规则。

对于如下的svc:

# kubectl describe svc/mysql
Name: mysql
Namespace: default
Labels: name=mysql
Selector: name=newdeploy
Type: ClusterIP
IP: 10.16.59.77
Port: <unnamed> 3306/TCP
Endpoints: 172.17.29.35:3306
Session Affinity: None
No events.

生成的iptables规则如下:

-A PREROUTING -m comment –comment “handle ClusterIPs; NOTE: this must be before the NodePort rules” -j KUBE-PORTALS-CONTAINER
-A PREROUTING -m addrtype –dst-type LOCAL -m comment –comment “handle service NodePorts; NOTE: this must be the last rule in the chain” -j KUBE-NODEPORT-CONTAINER
-A OUTPUT -m comment –comment “handle ClusterIPs; NOTE: this must be before the NodePort rules” -j KUBE-PORTALS-HOST
-A OUTPUT -m addrtype –dst-type LOCAL -m comment –comment “handle service NodePorts; NOTE: this must be the last rule in the chain” -j KUBE-NODEPORT-HOST

-A KUBE-PORTALS-CONTAINER -d 10.16.59.77/32 -p tcp -m comment –comment “default/mysql:” -m tcp –dport 3306 -j REDIRECT –to-ports 53407
-A KUBE-PORTALS-HOST -d 10.16.59.77/32 -p tcp -m comment –comment “default/mysql:” -m tcp –dport 3306 -j DNAT –to-destination 10.16.59.13:53407

(之所以在PREROUTING和OUTPUT设置相同的规则,是为了匹配从外部来的访问和发自本机的访问两种情况)

上述规则把访问 10.16.59.77:3306/TCP的请求,转到了本机的53407端口。用lsof检查可以发现该端口为kube-proxy进程。kube-proxy在各机器上会选择不同的端口以避免冲突。kube-proxy收到请求后,会转发给Service里定义的Endpoints

1.1版本开始,新增–proxy-mode=iptables模式,直接按相同比例把请求DNAT到指定的endpoint去。

Kubernetes Service的三种模式

  1. ClusterIP模式
  2. NodePort模式
  3. LoadBalancer模式

ClusterIP模式

生成一个 只在本cluster内有效的IP:port 组合,也就是仅对内暴露该服务。生成的IP范围由apiserver的–service-cluster-ip-range参数规定。

将生成的IP绑在一台运行着kube-proxy的机器上时,也可以对外提供服务。1.1版本的kube-proxy –proxy-mode=iptables时不能支持将clusterIP绑在Node上对外服务的做法。

NodePort模式

在所有Node上,用相同的端口号暴露服务。如Node的IP对cluster以外可见,则外部也可以访问该服务。

LoadBalancer模式

通知外部LoadBalancer生成 外部IP:port组合 ,并将请求转发进来,发给NodeIP:NodePort们。该行为听起来会把数据转发很多次。

该功能需要外部环境支持。目前GCE/GKE、AWS、OpenStack有插件支持该模式。

Posted in 默认分类 | Tagged | Leave a comment

Docker in docker的一些故障检查过程

术语约定:

  • Host:外层运行操作系统的机器
  • 外层daemon:Host上的docker daemon
  • 外层容器:外层daemon下辖的container,镜像启动时加–privileged参数。这个镜像的准备步骤是从docker下载当前1.9.1版安装(并固化到镜像里)CMD是一个脚本,先启动带debug选项的docker daemon 并放后台运行,然后pull并运行centos:7 一次,最后开启一个不停ping的命令,保持容器持续运行。通过docker exec 进入另行执行docker run命令测试内层是否可以正常启动
  • 内层daemon:外层容器里的docker daemon
  • 内层容器:内层daemon下辖的container

 

宋传义最近几周在尝试docker in docker,报告过几个问题,我在这里简要记录一下。因为在此docker in docker研究过程中我只是顾问的身份,并非主研人员,所以记述内容难免有缺乏背景介绍、阶段靠后等问题。宋传义报告的大量现象都是“最后一句错误信息”,但我的工作方式是从“第一条错误信息开始看”。

启动内层docker daemon时报告缺cgroup mount

宋传义报告在1.9上可以成功的在外层容器里运行内层的docker daemon,但1.7的报告缺cgroup mount。检查发现,Docker 1.7 并不会给内层容器 mount /sys/fs/cgroup/* 目录。只需要手工补mount即可混过去,满足启动docker daemon的需求。

在Docker 1.8.0的changelog里 Runtime 小节记述了这个变动:

  • Add cgroup bind mount by default

因此1.8.0以后都是可以的。CentOS 7.1.1503 早先版本带的是 1.7.1 后续升级到 1.8.2。后经催促,公司内网的安装源更新到新版本 。

不过1.8.2 RPM的docker-storage-setup脚本有问题 https://bugs.centos.org/view.php?id=9787 在未启用LVM的情况下会直接报错退出,无法从 /etc/sysconfig/docker-storage-setup 生成 /etc/sysconfig/docker-storage 配置文件。所以建议手工维持这俩文件的一致性。

在外层容器里启动内层容器时报告缺/sys/fs/cgroup/docker.service

这个故障,宋传义描述为“只有rz-ep17上docker in docker运行正常,其它机器均失败”。我尝试了一下,其它机器也不是全都失败,只是失败概率极高,偶尔还能遇到stack overflow;rz-ep17也不是每次都成功,但成功率极高。宋传义报告的故障现象为 docker run 失败,错误信息为 umount shm 和 umount mqueue失败。

首先双人交叉检查故障机和正常机的软件版本,发现Host内核、Docker外层daemon版本均精确一致、命令行精确一致;内层docker不管什么版本都能重现故障。听起来似乎是灵异现象。

 

尝试用fatrace、inotify-tools检查,发现fatrace在打开fanotify之后,IO事件发生后即收到File too large错误信息退出;而inotify直接就没动静。看起来这俩工具还不兼容container环境。

 

scytest 这个镜像启动时会在后台启动 start_docker.sh 它会在后台运行内层daemon。

在这个daemon环境下,用 docker run -ti 启动内层容器,则基本可以确保损毁当前运行的内层docker daemon,后续所有次数启动内层容器均会出现umount shm和umount mqueue失败的问题。后续我们发现是上次daemon出错时未能及时umount掉device-mapper设备,虽然下次daemon启动时会尝试清理,但还是没清理干净。手工umount掉/var/lib/docker/container/***ID***/{shm,mqueue} 即可修复。期间还尝试在外层容器里执行dmsetup remove_all 结果发现删除了容器里的device mapper之后,Host的device mapper设备节点漏进来了,是个安全隐患。

在这个daemon环境下,用 docker run -d 启动内层容器则大概率会成功。

如果kill掉start_docker.sh启动的docker daemon,手工在docker exec bash的命令行上另启动一个daemon,则一定出/sys/fs/cgroup/docker.service的问题。

于是问题逐渐清晰起来。

 

错误信息的文件名 docker.service 看起来“比较像systemd的命名风格”,所以我找了一下,发现在Host的cgroup目录里 /sys/fs/cgroup/systemd.slice/有个docker.service目录,但外层容器内的cgroup并没有这个。

搜源代码也搜不到docker.service 这个字符串,于是只能判定为该路径是从外部获取并拼装起来的。考虑到命令行精确一致,我又去看了看环境变量,也没有发现相关内容。

 

凝神定志,用重量级武器strace -f 跟踪内层docker daemon,记录下其文件访问行为,并比对错误信息,可以清晰的看到准备容器文件系统内容、mount、准备容器的cgroup环境、运行程序、失败、清理现场的过程,而且发现对 /sys/fs/cgroup/docker.service 的访问是由 内层daemon调用native exec driver 执行的,还未运行到启动容器内程序的步骤,也就是说,访问的是 外层容器内的/sys/fs/cgroup/docker.service 而不是 内层容器内的该文件。native exec driver目前是libcontainer/runc的一部分。我去GitHub搜源码,偶尔看到在https://github.com/opencontainers/runc/blob/3317785f562b363eb386a2fa4909a55f267088c8/libcontainer/cgroups/utils.go 中有分别读 /proc/1/cgroup 和/proc/self/cgroup 的两个函数,顿时敏感的意识到root cause就在这里。我赶紧去核对,发现 从CMD/ENTRYPOINT启动的start_docker.sh及其子进程docker daemon、子进程ping的/proc/<PID>/cgroup内容最后一行,和手工docker exec 进去执行的所有命令的该文件的最后一行内容不同:

image2015-12-4 8-40-34

上述路径前面拼上/sys/fs/cgroup/systemd/即为Host上的路径。

看起来应该是由于docker run设置了容器的cgroup环境,所以容器内原生的进程都基础此设置;而docker exec没有这个初始化过程,只是直接送一个进程在容器里执行,所以不同。

根据这个结论,宋传义进行了回归测试,终于可以100%重现失败过程,近100%重现成功过程(部分失败由于代码质量引起stackoverflow)

启动内层容器时报告缺/sys/fs/docker-daemon

错误信息 Error response from daemon: Cannot start container 8aa10e0596282a11b7d841f25355426e9a5e395cb980cf66ec89c9d1a439ae4d: [8] System error: mkdir /sys/fs/docker-daemon: no such file or directory

和上面那个故障相关

image2015-12-4 11-53-53

但因为mkdir: cannot create directory ‘/sys/fs/docker-daemon’: No such file or directory (/sys/fs/ 不是一个tmpfs而是/sys/的一部分;对比/sys/fs/cgroup/ 是个tmpfs可以随便写入)所以此问题无解

奇怪的是,我手工启动一个 daemon 其状态如下:

image2015-12-4 12-2-14

结果一样,还是出docker-daemon目录的错误。

可能这就是宋传义在CMD docker daemon和EXEC docker daemon之间来回切换的原因吧?

 

重启rz-ep16,然后查看,Host上docker服务刚启动时cgroup为

10:hugetlb:/
9:perf_event:/
8:blkio:/
7:net_cls:/
6:freezer:/
5:devices:/
4:memory:/
3:cpuacct,cpu:/
2:cpuset:/
1:name=systemd:/system.slice/docker.service

启动一个–privileged容器之后变成

10:hugetlb:/
9:perf_event:/
8:blkio:/system.slice/docker.service
7:net_cls:/
6:freezer:/
5:devices:/system.slice
4:memory:/system.slice
3:cpuacct,cpu:/system.slice/docker.service
2:cpuset:/
1:name=systemd:/system.slice/docker.service

这个看起来和一直伟光正的rz-ep17相同,且随后的实验都完全成功。

经实验,发现docker被kubelet依赖启动的时候,/proc/<PID>/cgroup 文件中perf_event、freezer、cpuset三行会是/docker-daemon;docker独立启动时则为/ 这样的正确内容。但这俩服务有强关联:systemctl restart docker重启还是错误内容;systemctl stop再start docker成功,但会导致kubelet服务停止。

外层容器首次yum会失败

image2015-12-4 12-41-40

稳定重现,原因不明。第二次就没事了

结论

  • 看错误信息要看第一条,而不是最后一条
  • 运维相关工具是检查不熟悉程序的行为的利器
  • 容器内和操作系统上的运行环境差异较大,除了fatrace\inotify失败,以后可能还会遭遇其它兼容性问题
  • Docker的坑还很多,尚处于初期开发阶段,变动很大,质量较差
  • 我们对 cgroup 的认识还太粗浅
  • 我们对devicemapper完全无认知
  • 编译型语言调试比较困难
Posted in 默认分类 | Tagged , | Leave a comment

Sentry新版SSO Provider讲解

从GH-1372 issue完成时开始,Sentry 7.x转向使用自家定义的SSO Provider,逐渐抛弃django-social-auth结构。因为缺乏文档,我在此事上消耗了大量的时间。现在写这篇Wiki用于记录:

基类:sentry/auth/provider.py 中的 Provider 虚基类

样例:sentry/auth/providers/ 目录下的dummy.py和oauth2.py 两个,以及sentry-sso-google 基于Sentry自带的OAuth2Provider

 

基本执行流程:

  1. 通过django机制加载app,在初始化时调用sentry.auth.manager的register方法,注册自己的名字和class
  2. 管理员在 Organization 的Auth页开启SSO时,会生成上述类的一个实例。Sentry的helper.py负责此流程,先调用get_auth_pipeline()函数,取得一个成员为多个 AuthView的超类 的list,然后对着这个list依次执行成员的 dispatch()函数
  3. 每个步骤按照pipeline的顺序执行一个AuthView.dispatch(self,request,helper)函数,其中request可以读到当前的django request内容。因为URL都为/auth/sso/ 所以需要根据querystring来判断执行到哪一步了,并收集本步的信息,调用 helper.bind_state() 保存起来,然后return help.next_step()跳到下一步执行
  4. 最后build_identify()会被执行,需要返回一个至少包含id、email、name的dict,此dict被Sentry用来构造用户信息。
Posted in 默认分类 | Tagged , | Leave a comment

sentry-公司内IM插件

这插件被大家催了很久了,到1月底终于决定动手做。

准备知识

公众号Pub服务API(内网wiki,链接已删除)

签名请求规范(内网wiki,链接已删除)

New Forms of Authentication

插件组成

  • setup.py 安装脚本。插件注册发现机制请参见
  • sentry_IM/__init__.py 确保这个目录是python package的文件。顺便查询一下VERSION供引用
  • sentry_IM/requests_APISign.py 上述New Forms of Authentication文档里提到的requests auth class,提供公司的Authencation验证header
  • sentry_IM/send_IM.py 发消息用的Class
  • sentry_IM/plugin.py 插件本身

程序讲解

插件本身继承自sentry.plugins.bases.notify.NotificationPlugin类,需要实现其notify(self, notification)方法。因基类里notify()调用了notify_users(),所以pylint会有提示。

参数notification为一个对象,可以从中取得event,然后从event取得所属的group以及group所属的project。根据project可以取得要通知的人员列表。(这里抄袭了MailPlugin的发送列表)因为这个函数得到的人员列表是Sentry里的User id,所以还需要再根据id查询到User,然后从其Email字段中取得localpart

最后,根据上述project.name、event.message、group.get_absolute_url() 组装其通知消息文本(URL在IM客户端渲染时才变成超链接,我这里只发URL本身),生成send_IM.py 的实例,对着上述人员列表发送消息文本。

IM插件所需的四个参数,在总的配置文件中列出,由插件生成send_IM实例时传递,然后由其传递给requests_APISign的实例,完成对HTTP请求的签名

配置是否开启

本来是想像MailPlugin那样直接在Account/Notification页做个列表供勾选。但阅读代码发现,这个功能并不是MailPlugin自己的功能,而是sentry做了特殊处理,先获得用户订阅的projects_list,然后循环生成ProjectEmailOptionsForm的list,随后再执行从各插件获取的ext_forms[]配置表单。ext_forms里每一个插件配置表单的初始化参数只有plugin、user和prefix,要想表达“所有可订阅的projects”这个概念会相当啰嗦。因此,最终确定为:表单里只勾选per-User 级别是否开启;而用户是否属于具体某个event/project的发送目标,则直接共用MailPlugin那边的选择,然后再用上述per-User级别开关做一遍过滤。这里两个插件之间略有粘连,主要是由于User-Project之间只有单向对应关系,逆向查询太罗嗦导致的。

image2016-2-3 16-17-40

最终发送效果

image2016-2-3 16-19-56

Github地址 https://github.com/julyclyde/sentry-IM

Posted in 默认分类 | Tagged , | Leave a comment

Sentry 新版插件基类讲解

根据 sentry/plugins/bases/__init__.py 的说明,新版插件分类四类,其中常见的:

  • NotificationPlugin 比如sentry_mail、sentry_webhook插件

NotificationPlugin的基类定义参见 sentry/plugins/bases/notify.py

Posted in 默认分类 | Tagged , | Leave a comment

Sentry整理杂记

本讨论均基于Sentry 7.7版本

插件机制

自带插件 src/sentry/plugins/ 每插件一个目录

自带插件loader:src/sentry/conf/server.py 里的INSTALLED_APPS tuple

外装插件loader:utils/runner.py 里的 install_plugins()函数,对iter_entry_points()遍历并将其加入INSTALLED_APPS tuple中

外装插件注册:插件的setup.py里执行注册entry_points的过程。例https://github.com/getsentry/sentry-groveio/blob/master/setup.py

参考setuptools:pkg_resources

列出本Python安装的Sentry插件
#!/usr/bin/env python
import pkg_resources
for ep in pkg_resources.iter_entry_points('sentry.apps'):
    print str(ep)
for ep in pkg_resources.iter_entry_points('sentry.plugins'):
    print str(ep)

 

sentry-jira插件

插件基本配置

每Project分别配置,需要输入jira的instance URL、用户名和密码。以上内容base64存在数据库sentry_projectoptions表里 where `key` like ‘jira%’

配置用户名密码之后,选择Sentry project关联到哪个JIRA Project,保存设置,并Enable Plugin即可。

 

Due Date问题

在Sentry Event页面右边点击Create JIRA Issue进入创建页面。但下面Due Date总提示Operation value must be a string 。

 点击展开故障详情

也就是说,如果不改sentry-jira插件,就无解。所以我提交了pull request https://github.com/thurloat/sentry-jira/pull/71

5日下午突然发现,目前线上Sentry 6.4.4的duedate显示为django的SelectDateWidget,而不像我自己的测试装Sentry 7.7一样直接用文本框。经过仔细对比,发现ops-sentry01上的sentry-jira插件是修改过的版本,forms.py文件class JIRAIssueForm新增了一段

site-packages/sentry_jira/forms.py片段
144
145
146
147
148
149
150
151
duedate = forms.DateField(
    label="duedate",
    #widget=adminwidgets.AdminDateWidget(),
    widget=SelectDateWidget(),
    initial=datetime.datetime.now(),
    #widget=DateWidget(usel10n=True)
    required=True
)

但这段代码没在公司的git库里保存,我就做了tree diff另外保存了。

SSO集成

厂家SSO讨论:https://github.com/getsentry/sentry/issues/1372

新版Sentry有auth backend基类https://github.com/getsentry/sentry/tree/master/src/sentry/auth/

SENTRY_FEATURES['organizations:sso']改为True可以开启Auth页面,设置sso。

目前,我参考sentry-sso-google写出来的sentry-sso-sankuai放在公司内网git库。

SENTRY_SINGLE_ORGANIZATION=True会导致/auth/login/ 跳转到 /auth/login/org_slug/ ,从而无法登录非SSO的用户(如系统自带的名为sentry的超级用户)。所以我在staging环境里关闭了这个参数。

Organizations

当SENTRY_SINGLE_ORGANIZATION=False时,utils/runner.py加载配置之后会将SENTRY_FEATURES[‘organizations:create’]强制改为False,从而禁用了右上角新创建Organization的“加号”链接。如果在此状态下删除了最后一个Organization,则其中的Team会变成游离状态,只能改掉参数重启服务重新创建org了,而且重建之后某种情况下会导致游离状态的Team丢失。切记不要删除Organization!!

根据getsentry原厂GH-1372号issue,每个Organization只能开启一个SSO AuthProvider。

目前看来,咱们的使用方法对多个Organization并没有需求。

 

新来的SSO用户默认属于所有Team的问题

经阅读代码文件web/frontend/accounts.py 发现在SENTRY_SINGLE_ORGNAZATION=True时会默认设置新注册用户的has_global_access值为True,然后如果有authprovider的话,再用authprovider.default_global_access更新该值;查看auth/helper.py 也发现会在登录和关联身份时,用 authprovider.default_global_access 给用户的has_global_access赋值。

2015年10月23日,将sentry_organizationmember表中 user_id in (35, 66, 67, 69, 71, 72, 73, 74, 77, 78, 81, 83, 84, 85, 86, 88, 89, 91, 92, 95, 96, 97, 98, 99, 100, 102, 103, 105, 107, 108, 109, 110, 112, 113, 114, 115, 117, 118, 119, 120, 122, 124, 125, 126, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 164, 166, 167, 168, 172, 173, 174, 175, 176, 177, 178, 180, 182, 183, 184, 185, 186, 187, 188, 189, 191, 192, 193, 194, 195, 196, 198, 199, 200, 203, 204, 205, 206, 207) 的111位用户的 has_global_access 值改为0,并将sentry_authprovider中id=2,provider=Sankuai的default_global_access字段改为0。

至于如何在SSO插件代码中设置,以便Sentry激活该SSO authprovider时自动将 sentry_authprovider 中 default_global_access字段设置为0,尚须进一步研究。将 orgnazation和authprovider关联的代码在web/frontend/organization_auth_settings.py 但其中并没有涉及default_global_access的内容,也许这个值并不是由authprovider影响的?

Team Admin无权批准人员加入的问题

这一代 Sentry 的权限系统设计的很烂,把 一个人属于哪个Team、一个人有权管理哪个Team混在一起了,无法表达“一个人是TeamA的member,同时是TeamB的admin”的意思。所以我一直倾向于授予较低的权限,但导致了事故。但10月29日由杨明芽发现 Team Admin 无法邀请、批准人员加入本Team,所以写了个后台任务定时批准requests。

Posted in 默认分类 | Tagged , | 1 Comment