Horizon 源码阅读(四)—— 调用Novaclient流程

一、写在前面

这篇文章主要介绍一下Openstack(Kilo)关于horizon 调用NovaClient的一个分析。

        如果转载,请保留作者信息。

        邮箱地址:jpzhang.ht@gmail.com


二、novaclient目录结构

novaclient/

         |---client.py --------主要提供HTTPClient类,也提供根据版本创建Client对象的函数

         |---base.py  --------提供基本的Manager基类

         |---shell.py  --------命令解析,创建相应版本的Client类对象,调用相应版本的shell.py中的函数

         ...

         |---v2

                |---client.py ---------版本Client类,拥有一系列Manager类对象,这些Manager可以调用相应的组件

                |---flavors.py --------具体的Manager类,使用HTTPClient对象与对应的组件进行通信

                ...

                |---shell.py   ————提供每个Command对应的方法


、以创建虚拟机为例分析源码

/openstack_dashboard/api/nova.py

horizon 调用APi创建虚拟机

def server_create(request, name, image, flavor, key_name, user_data,
                  security_groups, block_device_mapping=None,
                  block_device_mapping_v2=None, nics=None,
                  availability_zone=None, instance_count=1, admin_pass=None,
                  disk_config=None, config_drive=None, meta=None):
    return Server(novaclient(request).servers.create(
        name, image, flavor, userdata=user_data,
        security_groups=security_groups,
        key_name=key_name, block_device_mapping=block_device_mapping,
        block_device_mapping_v2=block_device_mapping_v2,
        nics=nics, availability_zone=availability_zone,
        min_count=instance_count, admin_pass=admin_pass,
        disk_config=disk_config, config_drive=config_drive,
        meta=meta), request)

返回一个创建后的Server对象,调用novaclient(request).servers.create()传入参数,发送创建虚拟机的请求。

novaclient(request).servers.create(

        name, image, flavor….), request


调用流程:

novaclient(request)-> servers -> create

1、novaclient(request):返回一个novaclient对象。

 /openstack_dashboard/api/nova.py

def novaclient(request):
    # 获取是否SSL证书检查,默认是禁用
    insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
    #获取CA证书使用来验证SSL连接,默认是None
    cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)

    #from novaclient.v2 import client as nova_client 返回Client类
    <strong>[1]</strong>c = nova_client.Client(request.user.username,
                           request.user.token.id,
                           project_id=request.user.tenant_id,
                           auth_url=base.url_for(request, 'compute'),
                           insecure=insecure,
                           cacert=cacert,
                           http_log_debug=settings.DEBUG)
    #设置Token ID 值
    c.client.auth_token = request.user.token.id
    #设置访问地址:例如 http://hty-nova:8774/v2/ea4d1859494c490495b027f174de307c
    c.client.management_url = base.url_for(request, 'compute')
    return c


novaclient/v2/__init__.py

from novaclient.v2.client import Client 

[1]处代码分析,返回一个Client对象

class Client(object):
    """
    顶级对象访问OpenStack计算API。
    Top-level object to access the OpenStack Compute API.

    """

    def __init__(self, username=None, api_key=None, project_id=None,
                 auth_url=None, insecure=False, timeout=None,
                 ...):

        password = api_key
        self.projectid = project_id
        ...

        # extensions 扩展
        self.agents = agents.AgentsManager(self)
        self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self)
        ...

        # Add in any extensions...在添加任何扩展
        if extensions:
            for extension in extensions:
                if extension.manager_class:
                    setattr(self, extension.name,
                            extension.manager_class(self))
        #构建HTTP客户端
        self.client = client._construct_http_client(
            username=username,
            password=password,
            ...
            **kwargs)
...

这个client里面有一个Client类,拥有一堆的Manager负责管理各种资源,只需引用这些Manager就可以操作资源,然后创建一系列的Manager类来负责处理资源,在这些Manager类中主要使用HTTPClient来发送请求对相应的组件进行操作,最后,将client版本能够实现的功能封装成函数,这些函数进而能够被相应的command调用。

2、novaclient(request).servers.create():

novaclient/v2/client.py

引用ServerManager操作server 

class Client(object):
	def __init__(self, username=None, api_key=None, project_id=None,…)
	         ….
                  #负责管理servers,只需引用Manager就可以操作servers
                   self.servers = servers.ServerManager(self)
                   …

novaclient/v2/servers.py

创建虚拟机create() 函数

class ServerManager(base.BootingManagerWithFind):
    resource_class = Server # 资源类,上文定义

    def create(self, name, image, flavor, meta=None, files=None,
               reservation_id=None, min_count=None,
               max_count=None, security_groups=None, userdata=None,
               key_name=None, availability_zone=None,
               block_device_mapping=None, block_device_mapping_v2=None,
               nics=None, scheduler_hints=None,
               config_drive=None, disk_config=None, **kwargs):
        """
        Create (boot) a new server.创建(启动)新的服务器。
        
        """
       #判断虚拟机创建数量
        if not min_count:
            min_count = 1
        if not max_count:
            max_count = min_count
        if min_count > max_count:
            min_count = max_count

        # 组拼参数
        boot_args = [name, image, flavor]

        boot_kwargs = dict(
            meta=meta, files=files, userdata=userdata,
            reservation_id=reservation_id, min_count=min_count,
            max_count=max_count, security_groups=security_groups,
            key_name=key_name, availability_zone=availability_zone,
            scheduler_hints=scheduler_hints, config_drive=config_drive,
            disk_config=disk_config, **kwargs)

       #block_device_mapping:(可选扩展)的块设备映射此虚拟机的字典。
        if block_device_mapping:
            resource_url = "/os-volumes_boot"
            boot_kwargs['block_device_mapping'] = block_device_mapping
        elif block_device_mapping_v2:
            resource_url = "/os-volumes_boot"
            boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2
        else:
            resource_url = “/servers”

        # nics(可选扩展)的NIC的有序列表要添加到该虚拟机,与有关连接的网络,固定的IP地址,端口等。
        if nics:
            boot_kwargs['nics'] = nics

        response_key = “server"
       #调用_boot()
        return self.<strong>_boot</strong>(resource_url, response_key, *boot_args,
                          **boot_kwargs)


    def <strong>_boot</strong>(self, resource_url, response_key, name, image, flavor,
              meta=None, files=None, userdata=None,
              reservation_id=None, return_raw=False, min_count=None,
              max_count=None, security_groups=None, key_name=None,
              availability_zone=None, block_device_mapping=None,
              block_device_mapping_v2=None, nics=None, scheduler_hints=None,
              config_drive=None, admin_pass=None, disk_config=None, **kwargs):
        """
        Create (boot) a new server. 创建(启动)新的服务器。
        """
        # 调用Restful API带的body参数
        body = {"server": {
            "name": name,
            "imageRef": str(base.getid(image)) if image else '',
            "flavorRef": str(base.getid(flavor)),
        }}
        if userdata:
            if hasattr(userdata, 'read'):
                userdata = userdata.read()

            if six.PY3:
                userdata = userdata.encode("utf-8")
            else:
                userdata = encodeutils.safe_encode(userdata)

            userdata_b64 = base64.b64encode(userdata).decode('utf-8')
            body["server"]["user_data"] = userdata_b64
        if meta:
            body["server"]["metadata"] = meta
        if reservation_id:
            body["server"]["reservation_id"] = reservation_id
        if key_name:
            body["server"]["key_name"] = key_name
        if scheduler_hints:
            body['os:scheduler_hints'] = scheduler_hints
        if config_drive:
            body["server"]["config_drive"] = config_drive
        if admin_pass:
            body["server"]["adminPass"] = admin_pass
        if not min_count:
            min_count = 1
        if not max_count:
            max_count = min_count
        body["server"]["min_count"] = min_count
        body["server"]["max_count"] = max_count

        if security_groups:
            body["server"]["security_groups"] = [{'name': sg}
                                                 for sg in security_groups]

        # Files are a slight bit tricky. They're passed in a "personality"
        # list to the POST. Each item is a dict giving a file name and the
        # base64-encoded contents of the file. We want to allow passing
        # either an open file *or* some contents as files here.
        if files:
            personality = body['server']['personality'] = []
            for filepath, file_or_string in sorted(files.items(),
                                                   key=lambda x: x[0]):
                if hasattr(file_or_string, 'read'):
                    data = file_or_string.read()
                else:
                    data = file_or_string

                if six.PY3 and isinstance(data, str):
                    data = data.encode('utf-8')
                cont = base64.b64encode(data).decode('utf-8')
                personality.append({
                    'path': filepath,
                    'contents': cont,
                })
      
        if availability_zone:
            body["server"]["availability_zone"] = availability_zone

        # Block device mappings are passed as a list of dictionaries
        if block_device_mapping:
            body['server']['block_device_mapping'] = \
                self._parse_block_device_mapping(block_device_mapping)
        elif block_device_mapping_v2:
            body['server']['block_device_mapping_v2'] = block_device_mapping_v2

        if nics is not None:
            # NOTE(tr3buchet): nics can be an empty list
            all_net_data = []
            for nic_info in nics:
                net_data = {}
                # if value is empty string, do not send value in body
                if nic_info.get('net-id'):
                    net_data['uuid'] = nic_info['net-id']
                if (nic_info.get('v4-fixed-ip') and
                        nic_info.get('v6-fixed-ip')):
                    raise base.exceptions.CommandError(_(
                        "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' may be"
                        " provided."))
                elif nic_info.get('v4-fixed-ip'):
                    net_data['fixed_ip'] = nic_info['v4-fixed-ip']
                elif nic_info.get('v6-fixed-ip'):
                    net_data['fixed_ip'] = nic_info['v6-fixed-ip']
                if nic_info.get('port-id'):
                    net_data['port'] = nic_info['port-id']
                all_net_data.append(net_data)
            body['server']['networks'] = all_net_data

        if disk_config is not None:
            body['server']['OS-DCF:diskConfig'] = disk_config
        # 调用父类基类Manager._create()方法
        # ServerManager->BootingManagerWithFind->ManagerWithFind->Manager
        return self.<strong>_create</strong>(resource_url, body, response_key,
                            return_raw=return_raw, **kwargs)

novaclient/base.py

class Manager(base.HookableMixin):
    """
    Managers interact with a particular type of API (servers, flavors, images,
    etc.) and provide CRUD operations for them.
    """
    # 管理者与特定类型的API(servers, flavors, images,etc)进行交互,并为他们提供CRUD操作。

    resource_class = None
    cache_lock = threading.RLock()

    def __init__(self, api):# api 即 Client对象,从<span style="font-family: Arial, Helvetica, sans-serif;">novaclient/v2/client.py:self.servers = servers.ServerManager(self)传入</span>
        self.api = api

    def <strong>_create</strong>(self, url, body, response_key, return_raw=False, **kwargs):
        # 运行指定类型的所有挂钩。
        self.run_hooks('modify_body_for_create', body, **kwargs)
        # self.api 即novaclient/v2/client.py:self.servers = servers.ServerManager(self) class -> Client(object)对象;
        # client: self.client HTTPClient客户端对象 novaclient/client.py->def _construct_http_client() -> class HTTPClient(object)->def post()
        # 发起post请求
        _resp, body = self.api.client.<strong>post</strong>(url, body=body)
        if return_raw:
            return body[response_key]

        with self.completion_cache('human_id', self.resource_class, mode="a"):
            with self.completion_cache('uuid', self.resource_class, mode="a"):
                return self.resource_class(self, body[response_key])

novaclient/client.py/class HTTPClient(object)

#调用POST 发送请求
def post(self, url, **kwargs):
        return self.<strong>_cs_request</strong>(url, 'POST', **kwargs)s


def <strong>_cs_request</strong>(self, url, method, **kwargs):
        if not self.management_url:
            self.authenticate()
        if url is None:
            # To get API version information, it is necessary to GET
            # a nova endpoint directly without "v2/<tenant-id>".
            magic_tuple = parse.urlsplit(self.management_url)
            scheme, netloc, path, query, frag = magic_tuple
            path = re.sub(r'v[1-9]/[a-z0-9]+$', '', path)
            url = parse.urlunsplit((scheme, netloc, path, None, None))
        else:
            if self.service_catalog:
                url = self.get_service_url(self.service_type) + url
            else:
                # NOTE(melwitt): The service catalog is not available
                #                when bypass_url is used.
                url = self.management_url + url

        # Perform the request once. If we get a 401 back then it
        # might be because the auth token expired, so try to
        # re-authenticate and try again. If it still fails, bail.
        try:
            kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
            if self.projectid:
                kwargs['headers']['X-Auth-Project-Id'] = self.projectid

            resp, body = self.<strong>_time_request</strong>(url, method, **kwargs)
            return resp, body
        """有可能出现没有认证的情况,需要先认证再发送请求 """
        except exceptions.Unauthorized as e:
            ...

def <strong>_time_request</strong>(self, url, method, **kwargs):
    start_time = time.time()
    resp, body = self.<strong>request</strong>(url, method, **kwargs)
    self.times.append(("%s %s" % (method, url),
                       start_time, time.time()))
    return resp, body


def <strong>request</strong>(self, url, method, **kwargs):
    """ 构造请求报文参数 """
    kwargs.setdefault('headers', kwargs.get('headers', {}))
    kwargs['headers']['User-Agent'] = self.USER_AGENT
    kwargs['headers']['Accept'] = 'application/json'
    if 'body' in kwargs:
        kwargs['headers']['Content-Type'] = 'application/json'
        kwargs['data'] = json.dumps(kwargs['body'])
        del kwargs['body']
    if self.timeout is not None:
        kwargs.setdefault('timeout', self.timeout)
    kwargs['verify'] = self.verify_cert

    self.http_log_req(method, url, kwargs)

    request_func = requests.request
    session = self._get_session(url)
    if session:
        request_func = session.request

    """ 这里使用了第三方的 requests 库,发起post请求"""
    resp = request_func(
        method,
        url,
        **kwargs)

    self.http_log_resp(resp)

    if resp.text:
        # TODO(dtroyer): verify the note below in a requests context
        # NOTE(alaski): Because force_exceptions_to_status_code=True
        # httplib2 returns a connection refused event as a 400 response.
        # To determine if it is a bad request or refused connection we need
        # to check the body.  httplib2 tests check for 'Connection refused'
        # or 'actively refused' in the body, so that's what we'll do.
        """ 根据请求返回的结果决定是否抛出异常 """
        if resp.status_code == 400:
            if ('Connection refused' in resp.text or
                    'actively refused' in resp.text):
                raise exceptions.ConnectionRefused(resp.text)
        try:
            body = json.loads(resp.text)
        except ValueError:
            body = None
    else:
        body = None
    """ 根据请求返回的结果决定是否抛出异常 """
    if resp.status_code >= 400:
        raise exceptions.from_response(resp, body, url, method)
    # 返回调用结果
    return resp, body






     
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值