β

Manila 网络模块

the 3rd. Place 751 阅读

最近在研究 OpenStack Manila 的代码,总结成这篇文章,介绍了 Manila Share 主要代码和设计,重点在其网络的实现上。

Manila 网络模块

tags: Manila Network


前言

Manila 是目前比较完善的一个 OpenStack PaaS 服务组件,它需要借助 Neutron 来完成其管理和网络连接,其网络架构主要如下图,本文会主要介绍 Manila 的网络组件代码,而不是其网络架构。

Manila 网络逻辑架构

对于一个类似的 PaaS 项目,其主要需求是在 Neutron 上启动属于自己的 Service Network 和 Service Port,项目的 Agent 可以通过 Service Port SSH 连接到处于 Service Network 的 Service VM。

启动

服务的启动

先看 Manila 的启动过程。Manila share 的启动没有像 Neutron 的 agent 一样直接运行特定的main()函数,而是在manila/bin/manila-service通过service完成实例化,实例化的类并不是一个hard-code的类,而是可以通过share_manager这个配置修改的,其运行过程如下:

#bin/manila-share.py
        server = service.Service.create(binary='manila-share')

#manila/service.py
class Service(object):
    def create(cls, host=None, binary=None, topic=None, manager=None,
               report_interval=None, periodic_interval=None,
               periodic_fuzzy_delay=None, service_name=None):
        if not topic:
            topic = binary
        if not manager:
            subtopic = topic.rpartition('manila-')[2]
            manager = CONF.get('%s_manager' % subtopic, None)

#manila/common/config.py
    cfg.StrOpt('share_manager',
               default='manila.share.manager.ShareManager',
               help='Full class name for the share manager.'),

Driver 的加载

实例化share_manager时,首先获得配置文件对象,然后加载 driver,默认为GenericShareDriver

#manila/share/manager.py
class ShareManager(manager.SchedulerDependentManager):
    def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
        if not share_driver:
            share_driver = self.configuration.share_driver
        self.driver = importutils.import_object(
            share_driver, self.db, configuration=self.configuration)

#manila/service.py
    cfg.StrOpt('share_driver',
               default='manila.share.drivers.generic.GenericShareDriver',
               help='Driver to use for share creation.'),

GenericShareDriver 中的 service_instance_manager

加载 Nova、Neutron API

加载GenericShareDriver时会维护一个ssh_connections字典和service_instance_manager,这个service_instance_manager将去调用 Nova、Neutron 等的 API,它需要至少指定一个 Service VM 的用户:

#manila/share/drivers/generic.py
class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
    def __init__(self, db, *args, **kwargs):
        self.ssh_connections = {}
        self.service_instance_manager = (
            service_instance.ServiceInstanceManager(
                self.db, driver_config=self.configuration))

#manila/share/drivers/service_instance.py
class ServiceInstanceManager(object):
    def __init__(self, db, *args, **kwargs):
        if not self.get_config_option("service_instance_user"):
            raise exception.ServiceInstanceException(_('Service instance user '
                                                       'is not specified'))
        self.compute_api = compute.API()
        self.neutron_api = neutron.API()

实例化 Neutron API 类时,Manila 会尝试去获取一些东西,首先尝试获得admin_tenant_id(这是neutron.API的一个用@property修饰的函数,如果当前没有一个 token 的话,它会用 neutron 的用户名和密码去 keystone 的 endpoint 获取 token,此时 neutron client 会执行_extract_service_catalog,设置auth_tenant_id,如果已经有了 token 说明 client 已经执行过这个过成立,就直接返回),以确定到 Neutron 的连通性,然后再获得 Service Network ID、加载 Interface Driver(默认为manila.network.linux.interface.OVSInterfaceDriver)、配置 Service instance 网络,如果在配置文件打开了connect_share_server_to_tenant_network,还会执行connect_share_server_to_tenant_network给 Service VM 增加一个到用户网络的虚拟网卡,下面为其关键部分代码:

#manila/share/drivers/service_instance.py
class ServiceInstanceManager(object):
    def __init__(self, db, *args, **kwargs):
        attempts = 5
        while attempts:
            try:
                self.service_tenant_id = self.neutron_api.admin_tenant_id
                break
        ...
        self.service_network_id = self._get_service_network()
        self.vif_driver = importutils.import_class(
            self.get_config_option("interface_driver"))()
        self._setup_connectivity_with_service_instances()
        self.connect_share_server_to_tenant_network = self.get_config_option(
            'connect_share_server_to_tenant_network')

#manila/network/neutron/api.py
class API(base.Base):
    @property
    def admin_tenant_id(self):
        if self.client.httpclient.auth_token is None:
            try:
                self.client.httpclient.authenticate()
            except neutron_client_exc.NeutronClientException as e:
                raise exception.NetworkException(code=e.status_code,
                                                 message=e.message)
        return self.client.httpclient.auth_tenant_id

获取 Service Network

_get_service_network()这个函数是同步化的,因为他会根据配置文件里的service_network_name来获取 Network,如果有多个就报错,如果没有就主动建立,为了避免多个实例同时进入,需要在这个函数上加锁(这里的锁只是一个本地锁,所以请尽量避免多节点同时启动 manila-share)。

#manila/share/drivers/service_instance.py
class ServiceInstanceManager(object):
    @utils.synchronized("service_instance_get_service_network", external=True)
    def _get_service_network(self):
        """Finds existing or creates new service network."""
        service_network_name = self.get_config_option("service_network_name")
        networks = [network for network in self.neutron_api.
                    get_all_tenant_networks(self.service_tenant_id)
                    if network['name'] == service_network_name]
        if len(networks) > 1:
            raise exception.ServiceInstanceException(_('Ambiguous service '
                                                       'networks.'))
        elif not networks:
            return self.neutron_api.network_create(self.service_tenant_id,
                                                   service_network_name)['id']
        else:
            return networks[0]['id']

重要的 _setup_connectivity_with_service_instances

Instance Driver 是 Manila 与 Service VM 连接用的驱动,主要实现了plugunplug这两个方法。
_setup_connectivity_with_service_instances()是 Mainila 网络模块里一个比较重要的函数,这个函数完成了创建 Service port(查找 device id 为 manila-share 的 port,如果数量大于 1 则报错,为空则创建一个并设置好其 device id、device owner 和 host_id。这个函数也是同步化的,头部加了@utils.synchronized的装饰器)、同步 port(检查 Service Network 下有无 port 没有加入的子网,如果有则将其加进去。这个函数也是同步化的)、将 port 连入 ovs 上的 br-int(如果已存在同名的 tap 设备会报错,否则是执行

ovs-vsctl -- --may-exist add-port br-int tapXXXXXXXX-XX 
        -- set Interface tapXXXXXXXX-XX type=internal 
        -- set Interface tapXXXXXXXX-XX external-ids:iface-id=PORT-ID 
        -- set Interface tapXXXXXXXX-XX external-ids:iface-status=active 
        -- set Interface tapXXXXXXXX-XX external-ids:attached-mac=%s

这个命令,然后执行ip link set tapXXXXXXXX-XX address XX:XX:XX:XX:XX:XX来设置 MAC 地址,最后将其 up 起来,注意当设备处于 UP 状态后,内核将会根据这个设备的 CIDR 地址自动添加路由表,manila 不会去维护这个路由表)、给 port 设置 ip 地址(先检查已有的 IP 地址,如果已有的 IP 里有需要的 IP 则保留,否则删掉)、检查路由表(先执行ip route list proto kernel dev tapXXXXXXXX-XX获取 Service Port 上的路由表,再挨个获取路由项对应的子网,再用ip list proto kernel match SUBNET获取对指定网段的路由表项,检查 Service port 是不是第一个,如果不是则将处于第一个的路由表项删掉然后再通过 append 添加)和清理过期设备(先获取所有 tap 开头的设备,然后检查其 CIDR 地址是否与现在的 Service Port 重复,如果有就执行 VIF Driver 的 unplug 方法)。

#/manila/share/drivers/service_instance.py
class ServiceInstanceManager(object):
    def _setup_connectivity_with_service_instances(self):
        port = self._get_service_port()
        port = self._add_fixed_ips_to_service_port(port)
        interface_name = self.vif_driver.get_device_name(port)
        self.vif_driver.plug(interface_name, port['id'], port['mac_address'])
        ip_cidrs = []
        for fixed_ip in port['fixed_ips']:
            subnet = self.neutron_api.get_subnet(fixed_ip['subnet_id'])
            net = netaddr.IPNetwork(subnet['cidr'])
            ip_cidr = '%s/%s' % (fixed_ip['ip_address'], net.prefixlen)
            ip_cidrs.append(ip_cidr)

        self.vif_driver.init_l3(interface_name, ip_cidrs)
        device = ip_lib.IPDevice(interface_name)
        device.route.pullup_route(interface_name)
        self._remove_outdated_interfaces(device)

服务的初始化检查

至此 Manager 的实例化过程基本结束。之后 service 会执行 start 方法(见附录一),将启动 RPC 服务、做定时状态报告等,在这里我们只关心其调用的 init_host

首先执行 Driver 的do_setup,前面我们知道我们的默认 Driver 是GenericShareDriver,在这里再一次执行了 Nova API 和 Neutron API 的实例化,然后获取 host 在这台机器上的所有 share,依次执行ensure_share方法,确认 Share 和规则的状态,最后向 scheduler 同步状态。

启动一个 Share

接受请求

启动一个 share 将从share.ShareManager.create_share进入,我们简单分析其过程,主要还是看网络方面的操作。首先需要获得 Share Network 的 ID,然后调用_provide_share_server_for_share获取 Share Server,这个函数内部定义了一个同名的 Nested Function 并返回。这个 Nested Function 的逻辑是这样的,首先根据 Host 和 Share Network 检查是否已存在可用的 Service VM,如果没有则现在数据创建一个 Share Server 的相应数据,再调用_setup_server到驱动的_setup_server真正创建 Service Server。

#manila/share/manager.py
class ShareManager(manager.SchedulerDependentManager):
    def create_share(self, context, share_id, request_spec=None,
                     filter_properties=None, snapshot_id=None):
        share_network_id = share_ref.get('share_network_id', None)
        elif share_network_id:
            try:
                share_server, share_ref = self._provide_share_server_for_share(
                    context, share_network_id, share_id)
        ...


    def _provide_share_server_for_share(self, context, share_network_id,
                                        share_id):
        @utils.synchronized("share_manager_%s" % share_network_id)
        def _provide_share_server_for_share():
            try:
                share_server = \
                    self.db.share_server_get_by_host_and_share_net_valid(
                        context, self.host, share_network_id)
            except exception.ShareServerNotFound:
                share_server = self.db.share_server_create(
                ...
            if not exist:
                # Create share server on backend with data from db
                share_server = self._setup_server(context, share_server)
                ...
            return share_server, share_ref
        return _provide_share_server_for_share()

    def _setup_server(self, context, share_server, metadata=None):
            network_info = self._form_server_setup_info(context, share_server,
                                                        share_network)
            server_info = self.driver.setup_server(network_info,
                                                   metadata=metadata)

创建 Share Server

Manager 的_form_server_setup_info首先要获取一次 Share Network 的 ID,然后调用 Driver 的allocate_network分配 IP,我们的 Driver 还是前面看到的GenericShareDriver,它不需要这个过程,所以直接跳过去了。然后重新获取一次 Share Network 和 Network info(包括 server_idcidrneutron_net_idnetwork_allocation等信息),再将network_info发给 Driver 的setup_server,因为GenericShareDriver的 Service VM 均由service_instance_manager管理,所以请求会再转给service_instance_managerset_up_service_instance方法最后到_create_service_instance真正建立虚拟机。

获取网络信息

首先要获取镜像、密码或密钥、安全组(由配置选项的service_instance_security_group决定,如果不存在则自己建立,开放 CIFS、NFS、SSH、Ping 所需要的端口/规则,这里 Manila 调用的是 Nova 的 API,而且直接获取所有再过滤,可以优化),然后获取网络信息:

  1. 先获取 Service Subnet,这个 Service Subnet 是 Manila 的 Service Network 下的子网,其名称均为”routed_to_xxx”这样,在获取时如果没有则尝试找一个没有用的(name 为空)子网,将其重命名返回,如果再没有就建立一个;
  2. 然后把用户的子网(Share Network)相连的路由器连接到获得的 Service Subnet;
  3. 在 Service Subnet 里建一个device owner为“manila”的 Port,作为 Service VM 用的 Port,如果打开了connect_share_server_to_tenant_network,还会再建一个 Public Port。

最后再一次执行前面在启动部分介绍过的_setup_connectivity_with_service_instances,完成 Service Port 到 Service VM 的连通。

#manila/share/drivers/generic.py
class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
    def setup_server(self, network_info, metadata=None):
        server = self.service_instance_manager.set_up_service_instance(
            self.admin_context,
            network_info['server_id'],
            network_info['neutron_net_id'],
            network_info['neutron_subnet_id'],
        )

#manila/share/drivers/service_instance.py
class ServiceInstanceManager(object):
    def set_up_service_instance(self, context, instance_name, neutron_net_id,
                                neutron_subnet_id):
        server = self._create_service_instance(context,
                                               instance_name,
                                               neutron_net_id,
                                               neutron_subnet_id)

    def _create_service_instance(self, context, instance_name, neutron_net_id,
                                 neutron_subnet_id):
            security_group = self._get_or_create_security_group(context)
            network_data = self._setup_network_for_instance(neutron_net_id,
                                                            neutron_subnet_id)
            ...
                self._setup_connectivity_with_service_instances()

    def _setup_network_for_instance(self, neutron_net_id, neutron_subnet_id):
        subnet_name = "routed_to_%s" % neutron_subnet_id
        service_subnet = self._get_service_subnet(subnet_name)

创建 Service VM 虚拟机

获取网络信息后,Manila 将拿着前面获取网络信息时得到的 Port 去创建 Service VM,在设置的max_time_to_build_instance时间内,不断尝试获取 Service VM 的状态,直到其成为Active状态。之后再将安全组设置到 Service VM,再在max_time_to_build_instance时间内尝试连接 Service VM 的 22 端口。

#manila/share/drivers/service_instance.py
class ServiceInstanceManager(object):
    def _create_service_instance(self, context, instance_name, neutron_net_id,
                                 neutron_subnet_id):
        service_instance = self.compute_api.server_create(
            context,
            name=instance_name,
            image=service_image_id,
            flavor=self.get_config_option("service_instance_flavor_id"),
            key_name=key_name,
            nics=[{'port-id': port['id']} for port in network_data['ports']])
        ...
        if security_group:
            self.compute_api.add_security_group_to_server(
                context,
                service_instance["id"], security_group.id)
        ...
        if not self._check_server_availability(service_instance):
            raise exception.ServiceInstanceException(

    def _check_server_availability(self, server):
        while time.time() - t < self.max_time_to_build_instance:
            try:
                socket.socket().connect((server['ip'], 22))
                return True
            except socket.error as e:
                time.sleep(5)
        return False

Service VM 创建完成

至此可以认为 Service VM 已经创建完成,代码将再返回到 manila.share.manager.ShareManager.create_share,继续进行下面的create_share的过程。

完成 Share 的创建

对于用的默认的GenericShareDriver,下面就是就是调用 Cinder API 创建 Volume、Attach Volume,SSH 到 Service VM 执行格式化,挂载等操作,与网络没有多少关系就不分析了,完成后一个 Share 就可以认为已经可以使用了。

#manila/share/manager.py
class ShareManager(manager.SchedulerDependentManager):
    def create_share(self, context, share_id, request_spec=None,
                     filter_properties=None, snapshot_id=None):
                ...
                export_location = self.driver.create_share(
                    context, share_ref, share_server=share_server)

#manila/share/drivers/generic.py
class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
    @ensure_server
    def create_share(self, context, share, share_server=None):
        """Creates share."""
        server_details = share_server['backend_details']
        volume = self._allocate_container(self.admin_context, share)
        volume = self._attach_volume(
            self.admin_context,
            share,
            server_details['instance_id'],
            volume)
        self._format_device(server_details, volume)
        self._mount_device(share, server_details, volume)
        location = self._get_helper(share).create_export(
            server_details,
            share['name'])
        return location

删除一个 Share

删除一个 share 将从share.ShareManager.delete_share进入,过程就比较简单了,先删除所有的 ACL 规则,然后对 Volume 执行 Unmount 和 Detach,最后调用 Cinder 的 API 将之删除。如果配置文件开启了delete_share_server_with_last_share那么当 Share Server 上没有 Share 时,就会删除这个 Share Server,包括调用 Nova API 删除 Service VM、移除路由器上的端口、将 Service Subnet 的 name 改为空,最后删除掉 Service Subnet 上所有 Port。因为 Service Port 上的 IP 和路由以后还可以用到,所以 Manila 不会去删除那些 IP 和路由项。

class ShareManager(manager.SchedulerDependentManager):
    def delete_share(self, context, share_id):
        ...
            for access_ref in rules:
                self._deny_access(context, access_ref, share_ref, share_server)
            self.driver.delete_share(context, share_ref,
                                     share_server=share_server)
        if CONF.delete_share_server_with_last_share:
            if share_server and not share_server.shares:
                self.delete_share_server(context, share_server)

class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
    def delete_share(self, context, share, share_server=None):
        if self._is_share_server_active(context, share_server):
            self._get_helper(share).remove_export(
                share_server['backend_details'], share['name'])
            self._unmount_device(share, share_server['backend_details'])
            self._detach_volume(self.admin_context, share,
                                share_server['backend_details'])
        self._deallocate_container(self.admin_context, share)

附录一:launcher 启动代码

launcher 启动部分代码,与其他 OpenStack 服务基本相同

#manila/bin/manila-share.py
            launcher.launch_server(server)

#manila/service.py
def launch_server(self, server, workers=1):
    wrap = ServerWrapper(server, workers)
    self.totalwrap = self.totalwrap + 1
    while (self.running and len(wrap.children) < wrap.workers
           and not wrap.failed):
        self._start_child(wrap)

class ServerWrapper(object):
    def __init__(self, server, workers):
        self.server = server
        self.workers = workers
        self.children = set()
        self.forktimes = []
        self.failed = False

class ProcessLauncher(object):
    def _start_child(self, wrap):
        pid = os.fork()
        if pid == 0:
            try:
                self._child_process(wrap.server)

    def _child_process(self, server):
        ...
        launcher = Launcher()
        launcher.run_server(server)

class Launcher(object):
    @staticmethod
    def run_server(server):
        server.start()
        server.wait()
作者:the 3rd. Place
MatheMatrix[PG]'s blog
原文地址:Manila 网络模块, 感谢原作者分享。

发表评论