在上一章中, 通过跟踪nova boot
命令, 已经完成了Instance创建参数的解析及身份认证的过程。这一章中继续完成CLI部分的代码跟踪,看看RESTful的请求是如何发出去的。
如果阅读过上一章的内容,就可知道nova boot
所对应的最终的执行函数是do_boot
。这段代码不长,内容也很简单,具体看注释。
do_boot
def do_boot(cs, args):
"""Boot a new server."""
#从args中把参数解析出来,并进行必要的有效性检查
boot_args, boot_kwargs = _boot(cs, args)
#从args中取出一些对应的扩展参数
extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args)
boot_kwargs.update(extra_boot_kwargs)
#使用servers对象创建Instance,并将结果返回
server = cs.servers.create(*boot_args, **boot_kwargs)
# Keep any information (like adminPass) returned by create
info = server._info
server = cs.servers.get(info[‘id‘])
info.update(server._info)
#以下部分对返回的结果做进一步的处理,然后打印给用户,和流程没有关系
flavor = info.get(‘flavor‘, {})
flavor_id = flavor.get(‘id‘, ‘‘)
info[‘flavor‘] = _find_flavor(cs, flavor_id).name
image = info.get(‘image‘, {})
if image:
image_id = image.get(‘id‘, ‘‘)
info[‘image‘] = _find_image(cs, image_id).name
else: # Booting from volume
info[‘image‘] = "Attempt to boot from volume - no image supplied"
info.pop(‘links‘, None)
info.pop(‘addresses‘, None)
utils.print_dict(info)
#如果设置了poll参数, 则一直轮询Instance的状态,一直到active结束
if args.poll:
_poll_for_status(cs.servers.get, info[‘id‘], ‘building‘, [‘active‘])
在这段代码中, 只有两个点需要进一步去跟踪
1. _boot中是怎么样去检查参数的?
2. servers是那个类的对象? 它又是如何去创建Instance的?
_boot
这是一段比较长的代码,但它完成的事情还是比较简单的, 对传入的各个参数进行合法性检查及一些冲突检测。因为参数比较多, 所以代码会比较长, 但每个参数几乎是独立的,所以到单个参数的时候都很好理解。
def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
"""Boot a new server."""
#对创建的实例个数进行检查, 没有设置,默认创建一个实例
if min_count is None:
min_count = 1
if max_count is None:
max_count = min_count
if min_count > max_count:
raise exceptions.CommandError("min_instances should be <= "
"max_instances")
if not min_count or not max_count:
raise exceptions.CommandError("min_instances nor max_instances should"
"be 0")
#取出image的信息
if args.image:
image = _find_image(cs, args.image)
else:
image = None
#如果没有设置image,则从image的metadata中查找image
if not image and args.image_with:
images = _match_image(cs, args.image_with)
if images:
# TODO(harlowja): log a warning that we
# are selecting the first of many?
image = images[0]
#对实例的flavor进行检查
if not args.flavor:
raise exceptions.CommandError("you need to specify a Flavor ID ")
if args.num_instances is not None:
if args.num_instances <= 1:
raise exceptions.CommandError("num_instances should be > 1")
max_count = args.num_instances
#取出flavor的信息
flavor = _find_flavor(cs, args.flavor)
#对meta 数据进行解析
meta = dict(v.split(‘=‘, 1) for v in args.meta)
#需要存入实例的文件,最多可以存入5个
files = {}
for f in args.files:
try:
dst, src = f.split(‘=‘, 1)
files[dst] = open(src)
except IOError as e:
raise exceptions.CommandError("Can‘t open ‘%s‘: %s" % (src, e))
except ValueError as e:
raise exceptions.CommandError("Invalid file argument ‘%s‘. File "
"arguments must be of the form ‘--file <dst-path=src-path>‘" % f)
# use the os-keypair extension
key_name = None
if args.key_name is not None:
key_name = args.key_name
#user数据的文件
if args.user_data:
try:
userdata = open(args.user_data)
except IOError as e:
raise exceptions.CommandError("Can‘t open ‘%s‘: %s" %
(args.user_data, e))
else:
userdata = None
#zone信息, zone用来对服务器进行分组, 这样可以满足一些资源的分组及优化管理
if args.availability_zone:
availability_zone = args.availability_zone
else:
availability_zone = None
#
if args.security_groups:
security_groups = args.security_groups.split(‘,‘)
else:
security_groups = None
#块设备(硬盘)
block_device_mapping = {}
for bdm in args.block_device_mapping:
device_name, mapping = bdm.split(‘=‘, 1)
block_device_mapping[device_name] = mapping
block_device_mapping_v2 = _parse_block_device_mapping_v2(args, image)
#只能有一个启动设备
n_boot_args = len(filter(None, (image, args.boot_volume, args.snapshot)))
have_bdm = block_device_mapping_v2 or block_device_mapping
# Fail if more than one boot devices are present
# or if there is no device to boot from.
if n_boot_args > 1 or n_boot_args == 0 and not have_bdm:
raise exceptions.CommandError(
"you need to specify at least one source ID (Image, Snapshot or "
"Volume), a block device mapping or provide a set of properties "
"to match against an image")
if block_device_mapping and block_device_mapping_v2:
raise exceptions.CommandError(
"you can‘t mix old block devices (--block-device-mapping) "
"with the new ones (--block-device, --boot-volume, --snapshot, "
"--ephemeral, --swap)")
#网卡信息
nics = []
for nic_str in args.nics:
err_msg = ("Invalid nic argument ‘%s‘. Nic arguments must be of the "
"form --nic <net-id=net-uuid,v4-fixed-ip=ip-addr,"
"port-id=port-uuid>, with at minimum net-id or port-id "
"specified." % nic_str)
nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""}
for kv_str in nic_str.split(","):
try:
k, v = kv_str.split("=", 1)
except ValueError as e:
raise exceptions.CommandError(err_msg)
if k in nic_info:
nic_info[k] = v
else:
raise exceptions.CommandError(err_msg)
if not nic_info[‘net-id‘] and not nic_info[‘port-id‘]:
raise exceptions.CommandError(err_msg)
nics.append(nic_info)
#hint信息, hint是用来影响调度器的,通过它可以来影响调度器的调度规则,从而实现一些自定义的调度功能。在进入调度器时, 可以深入讨论这个问题。
hints = {}
if args.scheduler_hints:
for hint in args.scheduler_hints:
key, _sep, value = hint.partition(‘=‘)
# NOTE(vish): multiple copies of the same hint will
# result in a list of values
if key in hints:
if isinstance(hints[key], basestring):
hints[key] = [hints[key]]
hints[key] += [value]
else:
hints[key] = value
#组成启动实例必须的三个参数
boot_args = [args.name, image, flavor]
if str(args.config_drive).lower() in ("true", "1"):
config_drive = True
elif str(args.config_drive).lower() in ("false", "0", "", "none"):
config_drive = None
else:
config_drive = args.config_drive
boot_kwargs = dict(
meta=meta,
files=files,
key_name=key_name,
reservation_id=reservation_id,
min_count=min_count,
max_count=max_count,
userdata=userdata,
availability_zone=availability_zone,
security_groups=security_groups,
block_device_mapping=block_device_mapping,
block_device_mapping_v2=block_device_mapping_v2,
nics=nics,
scheduler_hints=hints,
config_drive=config_drive)
return boot_args, boot_kwargs
cs.servers.create
先看看servers是那个类的对象。在V1.1的Client类中, 可以找到下面一行
self.servers = servers.ServerManager(self)
再看ServerManager的create函数, 在这个函数中,只是对实例个数进行了一个简单的检查。然后就调用了内部的_boot
函数。
但这里有个地方要注意, 我们没用任何block设备,所以实际的URL是/servers
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):
# TODO(anthony): indicate in doc string if param is an extension
# and/or optional
"""
Create (boot) a new server.
:param name: Something to name the server.
:param image: The :class:`Image` to boot with.
:param flavor: The :class:`Flavor` to boot onto.
:param meta: A dict of arbitrary key/value metadata to store for this
server. A maximum of five entries is allowed, and both
keys and values must be 255 characters or less.
:param files: A dict of files to overrwrite on the server upon boot.
Keys are file names (i.e. ``/etc/passwd``) and values
are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
:param userdata: user data to pass to be exposed by the metadata
server this can be a file type object as well or a
string.
:param reservation_id: a UUID for the set of servers being requested.
:param key_name: (optional extension) name of previously created
keypair to inject into the instance.
:param availability_zone: Name of the availability zone for instance
placement.
:param block_device_mapping: (optional extension) A dict of block
device mappings for this server.
:param block_device_mapping_v2: (optional extension) A dict of block
device mappings for this server.
:param nics: (optional extension) an ordered list of nics to be
added to this server, with information about
connected networks, fixed ips, port etc.
:param scheduler_hints: (optional extension) arbitrary key-value pairs
specified by the client to help boot an instance
:param config_drive: (optional extension) value for config drive
either boolean, or volume-id
:param disk_config: (optional extension) control how the disk is
partitioned when the server is created. possible
values are ‘AUTO‘ or ‘MANUAL‘.
"""
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)
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"
if nics:
boot_kwargs[‘nics‘] = nics
response_key = "server"
return self._boot(resource_url, response_key, *boot_args,
**boot_kwargs)
_boot
这段代码看起来很长, 但是做的事情还是很简单的,就是对RESTful的body数据进行封装,对一些非ASCII码的内容进行BASE64编码。然后调用_create进行创建。
def _boot(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.
:param name: Something to name the server.
:param image: The :class:`Image` to boot with.
:param flavor: The :class:`Flavor` to boot onto.
:param meta: A dict of arbitrary key/value metadata to store for this
server. A maximum of five entries is allowed, and both
keys and values must be 255 characters or less.
:param files: A dict of files to overrwrite on the server upon boot.
Keys are file names (i.e. ``/etc/passwd``) and values
are the file contents (either as a string or as a
file-like object). A maximum of five entries is allowed,
and each file must be 10k or less.
:param reservation_id: a UUID for the set of servers being requested.
:param return_raw: If True, don‘t try to coearse the result into
a Resource object.
:param security_groups: list of security group names
:param key_name: (optional extension) name of keypair to inject into
the instance
:param availability_zone: Name of the availability zone for instance
placement.
:param block_device_mapping: A dict of block device mappings for this
server.
:param block_device_mapping_v2: A dict of block device mappings V2 for
this server.
:param nics: (optional extension) an ordered list of nics to be
added to this server, with information about
connected networks, fixed ips, etc.
:param scheduler_hints: (optional extension) arbitrary key-value pairs
specified by the client to help boot an instance.
:param config_drive: (optional extension) value for config drive
either boolean, or volume-id
:param admin_pass: admin password for the server.
:param disk_config: (optional extension) control how the disk is
partitioned when the server is created.
"""
body = {"server": {
"name": name,
"imageRef": str(getid(image)) if image else ‘‘,
"flavorRef": str(getid(flavor)),
}}
#userdata是一个文件句柄,所以需要读出来并进行编码
if userdata:
if hasattr(userdata, ‘read‘):
userdata = userdata.read()
userdata = strutils.safe_encode(userdata)
body["server"]["user_data"] = base64.b64encode(userdata)
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.
#文件信息的userdata有相同的问题
if files:
personality = body[‘server‘][‘personality‘] = []
for filepath, file_or_string in files.items():
if hasattr(file_or_string, ‘read‘):
data = file_or_string.read()
else:
data = file_or_string
personality.append({
‘path‘: filepath,
‘contents‘: data.encode(‘base64‘),
})
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:
# Append the image to the list only if we have new style BDMs
if image:
bdm_dict = {‘uuid‘: image.id, ‘source_type‘: ‘image‘,
‘destination_type‘: ‘local‘, ‘boot_index‘: 0,
‘delete_on_termination‘: True}
block_device_mapping_v2.insert(0, bdm_dict)
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‘):
net_data[‘fixed_ip‘] = nic_info[‘v4-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
return self._create(resource_url, body, response_key,
return_raw=return_raw, **kwargs)
_create
这个函数就非常简单了,直接发送post请求。然后对返回结果进行解析。
其中post请求是由HTTPClient完成的。
def _create(self, url, body, response_key, return_raw=False, **kwargs):
self.run_hooks(‘modify_body_for_create‘, body, **kwargs)
_resp, body = self.api.client.post(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])
HTTPClient post
这里我一次性按函数的执行顺序把代码全列在下面。
def post(self, url, **kwargs):
return self._cs_request(url, ‘POST‘, **kwargs)
def _cs_request(self, url, method, **kwargs):
#management_url是认证时返回的结果,在前一章中,已经完成。
if not self.management_url:
self.authenticate()
# 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:
#X-Auth-Token, 这是认证返回的token值,也是权限认证的钥匙
kwargs.setdefault(‘headers‘, {})[‘X-Auth-Token‘] = self.auth_token
#对应的project-id, 也就是tenant-id
if self.projectid:
kwargs[‘headers‘][‘X-Auth-Project-Id‘] = self.projectid
#这里的url=‘/servers‘
resp, body = self._time_request(self.management_url + url, method,
**kwargs)
return resp, body
except exceptions.Unauthorized as e:
try:
# frist discard auth token, to avoid the possibly expired
# token being re-used in the re-authentication attempt
self.unauthenticate()
self.authenticate()
kwargs[‘headers‘][‘X-Auth-Token‘] = self.auth_token
resp, body = self._time_request(self.management_url + url,
method, **kwargs)
return resp, body
except exceptions.Unauthorized:
raise e
def _time_request(self, url, method, **kwargs):
start_time = time.time()
resp, body = self.request(url, method, **kwargs)
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
return resp, body
def request(self, url, method, **kwargs):
#准备HTTP头
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)
self.http_log_req((url, method,), kwargs)
#发送HTTP请求,并等待返回结果
resp = self.http.request(
method,
url,
verify=self.verify_cert,
**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:
pass
body = None
else:
body = None
if resp.status_code >= 400:
raise exceptions.from_response(resp, body, url, method)
return resp, body
到此为止, 所有CLI的操作基本完成,剩下的就是一些打印的轮询的操作,和我们的主要流程没有太大关系。此后就不再继续。
在下一章中, 将进入nova部分,看看齜的创建过程是什么样的。