Openstack Nova(六)----Instance 创建(CLI RESTful请求)

在上一章中, 通过跟踪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部分,看看齜的创建过程是什么样的。

时间: 2024-10-10 02:19:56

Openstack Nova(六)----Instance 创建(CLI RESTful请求)的相关文章

使用nova boot命令创建openstack实例

使用命令:nova boot --flavor 1 --key_name mykey--image 9e5c2bee-0373-414c-b4af-b91b0246ad3b --security_group default cirrOS 其中: flavor是虚拟机的配置,比如说内存大小,硬盘大小等,默认下1为最小,4为最大. key_name是创建虚拟机使用的密钥,使用以下三条命令创建密钥: ssh-keygen cd.ssh nova keypair-add --pub_key id_rsa

openstack之虚拟机的创建流程

这篇博文静静的呆在草稿箱大半年了,如果不是因为某些原因被问到,以及因为忽略它而导致的损失,否则我也不知道什么时候会将它完成.感谢这段时间经历的挫折,让我知道不足,希望你能给我更大的决心! 本文试图详细地描述openstack创建虚拟机的完整过程,从用户发起请求到虚拟机成功运行,包括客户端请求的发出.keystone身份验证.nova-api接收请求.nova-scheduler调度.nova-computer创建.nova-network分配网络.对于每一个模块在创建虚拟机的过程中所负责的功能和

【openstack N版】——创建云主机

一.启动实例 1.1 已准备服务介绍 MySql:为各个服务提供数据存储. RabbitMQ:为各个服务之间通信提供交通枢纽. keystone:为各个服务之间通信提供认证和服务注册. Glance:为虚拟机提供镜像管理. Nova:为虚拟机提供计算资源. Neutron:为虚拟机提供网络资源. 1.2 网络(flat) 1.2.1创建虚拟网络 1 #share 允许所有项目使用虚拟网络 2 [[email protected] ~]# openstack network create --sh

深挖Openstack Nova - Scheduler调度策略

深挖Openstack Nova - Scheduler调度策略 一.  Scheduler的作用就是在创建实例(instance)时,为实例选择出合适的主机(host).这个过程分两步:过滤(Fliter)和计算权值(Weight) 1. 过滤: 过滤掉不符合我们的要求,或镜像要求(比如物理节点不支持64bit,物理节点不支持Vmware EXi等)的主机,留下符合过滤算法的主机集合. 2. 计算权值 通过指定的权值计算算法,计算在某物理节点上申请这个虚机所必须的消耗cost.物理节点越不适合

Openstack nova代码部分凝视一

做个一个没怎么学过python的菜鸟.看源代码是最好的学习方式了,如今就从nova入手,主要凝视一下 nova/compute/api.py 中的 create_instance函数 def _create_instance(self, context, instance_type, image_href, kernel_id, ramdisk_id, min_count, max_count, display_name, display_description, key_name, key_d

Openstack nova代码部分注释一

做个一个没怎么学过python的菜鸟,看源码是最好的学习方式了,现在就从nova入手,主要注释一下 nova/compute/api.py 中的 create_instance函数 def _create_instance(self, context, instance_type, image_href, kernel_id, ramdisk_id, min_count, max_count, display_name, display_description, key_name, key_da

通过beego快速创建一个Restful风格API项目及API文档自动化(转)

通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界面. 一.创建数据库及数据表(MySQL) #db--jeedev -- ---------------------------- -- Table structure for `app` -- ---------------------------- DROP TABLE IF EXISTS `a

十六、创建RDS 证书模板

十六.创建RDS 证书模板 完成了VDI的标准部署.客户端需要通过HTTPS 协议访问.使用HTTPS 协议就需要在RDweb 和RD网关上绑定证书.在此,为RD 重新创建一个证书模板. 在运行中输入certsrv.msc ,打开证书颁发机构管理工具,如图 2.  在证书颁发机构管理工具中,选择证书模板---管理,如图 3.  在证书模板界面,选择计算机模板---复制模板,如图 4.  在新模板的属性对话框,选择 兼容性,如图 5.  在常规对话框,设置模板显示名称.模板名.有效期.并勾选"在A

Openstack Nova 二次开发之Nova-extend服务实现并启动验证

 Openstack Nova 二次开发之Nova-extend service 扩展 主要是讲如何启动openstack nova-extend services,该服务用于Openstack 二次扩展及部分需求开发,例如 ,节点巡检,动态迁移(基于FUSE 文件系统实现,分布式系统,如MooseFS),文件注入,Nova 服务的自身修复,instances IO 控制,instances CPU 隔离技术实现等其他需求开发 第一章:如何create openstack nova-extend