一、写在前面
这篇文章主要介绍一下Openstack(Kilo)关于horizon 调用NovaClient的一个分析。
如果转载,请保留作者信息。
邮箱地址:[email protected]
二、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