分享

用 Python 在 IaaS 平台中实现灵活的拓扑部署流程

fc013 发表于 2016-7-30 20:39:14 [显示全部楼层] 只看大图 回帖奖励 阅读模式 关闭右栏 0 7005


问题导读:

1.什么是IaaS?
2.拓扑部署的步骤是什么?
3.怎样将复杂的流程分解为多个功能单一的步骤?




应用背景IaaS
IaaS(Infrastructure as a service) 是以提供基础设施为主要服务一种云计算模型。基础设施通常包括计算资源(CPU)、存储资源(内存、磁盘)以及网络资源(网卡、交换机、路由)。这些资源可以是真实存在的,也可以是由虚拟化技术创建出来的,但 IaaS 服务的用户并不关心这一点。通常用户在向 IaaS 平台发起的请求中描述他们所需要的基础设施的要求,例如 CPU 大小、内存和磁盘的容量以及 IP 地址。而 IaaS 平台负责提供满足用户要求的机器(可能是实体机也可能是虚机),并确保其高可用性。

OpenStack
OpenStack 是一套用 Python 语言编写的开源云平台,被广泛用于提供 IaaS 服务。在架构上,OpenStack 用不同的组件来管理不同种类的基础资源,每个组件可以独立部署运行,并通过消息队列和 REST API 进行交互。这种架构使得 OpenStack 具有很高的可扩展性,开发者能够通过增加新的组件来扩展 OpenStack 的功能,以满足用户新的需求。Heat 便是为了提供拓扑部署功能而加入的组件。

Chef
Chef 是一套开源的软件部署和配置工具,分为服务器端和客户端。用户使用一种用 Ruby 语言定义的 DSL(Domain Specific Language)来描述具体的软件安装和配置过程,称为 Recipe。当用户向 Chef 服务器端发起软件安装的请求时,服务器端会在目标机器上安装 Chef 客户端,然后执行与软件对应的 Recipe 来完成软件的安装和配置。当前,Chef 被广泛用于在 IaaS 平台中提供软件部署的功能。

拓扑部署
对于企业用户而言,在 IaaS 上部署自己的应用是一种较为常见的需求。通常这需要申请多台不同配置的机器,并在每台机器上安装特定的软件,而传统的 IaaS 服务无法满足这些要求。拓扑便是针对这一问题对传统 IaaS 服务的一种延伸。拓扑定义了部署特定应用所需要的所有基础设施、软件以及它们之间的相互关系,如图 1 所示。一次拓扑部署便可以让用户获得部署应用所需要的一切基础设施,以及运行在这些基础设施之上的软件。

OpenStack 的 Heat 组件的出现,解决了拓扑部署中的创建基础设施的问题。Heat 提供基于 YAML 格式的模板语言 HOT(Heat Orchestration Template)来描述拓扑中的基础设施,并将这些描述解析成其他组件能够识别的输入,利用其他现有组件来完成基础设施的创建,如图 2 所示。Heat 的开发者计划在 Heat 的未来版本中提供完善的软件部署功能,届时 Heat 将成为完整的拓扑部署解决方案。

本文将结合 Heat 和 Chef 来实现拓扑部署的功能,但不会过多涉及 Heat 和 Chef 的 API 调用细节,而是将重点放在如何使得这一实现变得更加灵活、可扩展。

图 1. 拓扑定义示例
img001 (1).png

图 2. Heat 与其他组件的关系
img002.png

需求分析基本功能图 3. 拓扑部署流程
2Uniauf (1).png

一次拓扑部署大体上需要经历如下步骤,如图 3 所示:

  • 解析拓扑定义:用户通过图形界面编辑拓扑定义,保存为某种特定格式(如 YAML、XML) 作为拓扑部署功能的输入。拓扑定义包含基础设施定义和软件定义。
  • 创建基础设施:这部分功能可以直接利用 Heat 完成。
  • 完成软件部署,这一步有几个关键点需要注意:
       确保 Chef Server 和目标机器的连通性。因为目标机器可能存在于用户定制的私有网络里,而 Chef 服务器需要被所有机器共享,处于共享网络里。需要利用 OpenStack 网络组件 Neutron 提供的 Floating IP,使得私有网络里的机器能够访问共享网络。
       软件的安装顺序必须符合软件的依赖关系。例如有的应用软件依赖于数据库,那么就必须先安装数据库,再安装应用软件。
       对于不存在依赖关系,并且安装在不同机器上的软件,应该并发进行安装以节省时间。

其他功能

除了基本功能之外,还有一些附加功能也非常重要:

  • 日志功能:完成拓扑部署需要执行诸多操作,每当完成一个重要的操作时,记录该操作的相关信息,这样用户在等待部署完成的过程中可以了解部署的进展。一旦出错,也可以知道是在哪一个步骤出现问题。
  • 从失败中恢复:拓扑部署一个是相对耗时的过程,如果在一次部署的后期发生错误,当排除了该错误之后,要推翻前期已经完成的步骤而从头开始的话,无疑会造成令人沮丧的用户体验。理想情况下,应该允许部署只从上次失败的地方开始,继续完成剩余的部分。
  • 资源锁定功能:拓扑部署涉及多个基础设施资源,这些资源可以是已经存在的,也可以是新创建的。在拓扑部署的过程中,不能允许用户对这些资源执行其他操作,尤其是破坏性的操作。否则部署过程很有可能无法顺利完成。

接口设计Step 接口
依据前面的分析,拓扑部署流程可以分为多个执行步骤,例如创建基础设施、分配 Floating IP、部署软件、回收 Floating IP。清单 1 展示了以“步骤”这一概念为核心所设计的接口。

清单 1. The Step Interface
[mw_shl_code=python,true]class Step(object):
def execute(self, ctx, manager, target, **kwargs):
return NotImplemented[/mw_shl_code]

Step 通过唯一的方法 execute 定义了一个步骤最基本的行为,它包含如下参数:

  • ctx:这是一个字典类型,它包含了一次特定的拓扑部署的上下文信息。使用参数取代成员变量来保存上下文信息,使得 Step 的实例成为无状态对象。这样同一个 Step 对象可以同时执行多个部署。
  • manager:实现了拓扑部署过程中需要执行的具体操作,例如调用 OpenStack 和 Chef 的 API 完成相关功能。这些具体操作的实现非常复杂,Step 将这些操作代理给 manager 实现,可以避免自己变得过于臃肿,同时可以专注于实现部署流程。
  • target:这是拓扑定义的一部分。前文展示过,拓扑定义包含基础设施定义,软件定义等部分,当每个部分都已得到适当的处理时,拓扑部署也就完成了。因此,每个步骤都需要处理拓扑定义的一个特定部分,例如创建基础设施的步骤就需要处理基础设施定义这个部分。
  • kwargs:这个参数利用了 Python 中可变参数的特性。因为在每个步骤的执行过程中,可能会产生下一个步骤必须了解的信息,例如分配 Floating IP 这个步骤所产生的 Floating IP 地址,就是下一个步骤部署软件所必须知道的,否则 Chef Server 就无法连接目标机器。

StepImpl 基类

Step 接口并未包含任何实现,那么一个步骤如何执行呢,这由 StepImpl 定义,如清单 2 所示。可以看到 StepImpl 继承了 Step,并且提供了对 execute 的初步实现。该实现包含如下步骤:

  • 检查是否可以跳过该步骤:如果发现该步骤要完成的目标已经达成,则跳过。最典型的例子是分配 Floating IP 的步骤,如果目标机器已经有了 Floating IP,则无需分配。
  • 检查前置条件:若前置条件无法满足,则报错并停止执行剩余步骤。例如对于安装软件步骤来说,如果发现目标机器没有被分配 Floating IP,则无法继续执行。
  • 执行该步骤的具体逻辑:这一步留给每个特定步骤去实现。
  • 步骤完成后的其他操作:有些操作必须在步骤完成后才能执行,例如锁定目标机器的操作,必须在 Heat 创建完目标机器后才能执行。

上面每一步都有与之对应的_update_status 调用,将该步骤的执行情况记录下来。这样使得用户很容易追踪一次部署的进度。此外 StepImpl 还定义了一个 get_kwargs 方法,这是用来定义下一个步骤的输入参数的。下文会再次提到。

清单 2. The StepImpl Class
[mw_shl_code=python,true]class StepImpl(Step):
__metaclass__ = StepImplMeta
。。。

def execute(self, ctx, manager, target, **kwargs):
try:
result = self._skip(ctx, manager, target, **kwargs)
if result:
LOG.debug('%s: post condition already satisfied, will skip this step.' % self)
elif not self._pre_check(ctx, manager, target, **kwargs):
LOG.debug('%s: preconditions not satisfied.' % self)
self._update_status(ctx, manager,
target, self.status_def.pre_check_failed)
raise PreCheckFailed(self)
else:
LOG.debug('%s: start...' % self)
self._update_status(ctx, manager, target, self.status_def.started)
result = self._handle(ctx, manager, target, **kwargs)
LOG.debug('%s: end...' % self)
self._post_action(ctx, manager, target, result, **kwargs)
self._update_status(ctx, manager, target, self.status_def.succeed)
return result
except Exception as e:
details = traceback2.format_exc()
LOG.error(details)
self._update_status(ctx, manager,
target, self.status_def.failed, details=details)
raise e

def get_kwargs(self, ctx, manager, target, result):
return NotImplemented[/mw_shl_code]

StepWrapper
StepImpl 的作用是实现拓扑部署流程中的具体步骤,而 StepWrapper 的作用是将这些步骤整合起来,实现完整的部署流程,如清单 3 所示。一个 StepWrapper 对象可以包含多个 Step 对象,并在执行时依次执行这些 Step 对象。因此可以使用 StepWrapper 来定义多个步骤的执行顺序,此外它还有更多妙用,下文将会提到。

清单 3. The StepWrapper Class
[mw_shl_code=python,true]class StepWrapper(Step):
...
def __init__(self, *steps):
self.steps = steps

def execute(self, ctx, manager, target, **kwargs):
if len(self.steps) == 1:
result = self.steps[0].execute(ctx, manager, target, **kwargs)
else:
params = kwargs
for step in self.steps:
result = step.execute(ctx, manager, target, **params)
params = step.get_kwargs(ctx, manager, target, result)
target = params.pop('target')
return result[/mw_shl_code]

实现步骤实现:分配 Floating IP
这里分配 Floating IP 的步骤为例,展示 StepImpl 的一个具体实现。该实现主要包含三个方法,如清单 4 所示:

_handle:完成该步骤的具体功能,即分配 Floating IP。首先通过_get_server_info 方法获得目标机器的详细信息,从中拿到该机器一个网卡的 mac 地址,然后将该地址作为参数来绑定 Floating IP。绑定成功后,注册一个解绑 Floating IP 的回调,在部署完成后触发。最后返回分配的 Floating IP 和目标机器信息。

_skip:在_handle 执行前判断是否可以直接跳过此步骤。仍是先获得目标机器的信息,如果该机器上已经存在 Floating IP,则直接返回。注意,这里的返回值是与_handle 的返回值完全一致的。如果这里已经提供了所需的返回值,_handle 就无需执行。

get_kwargs:将 Floating IP 传递给下一个步骤作为输入参数。

清单 4. The AssociatingFloatingIP Class
[mw_shl_code=python,true]class AssociateFloatingip(StepImpl):

def _handle(self, ctx, manager, software, **kwargs):
server = self._get_server_info(ctx, manager, software.hostedOn)
mac_address = server.get_mac_address()
floatingip, callback = manager.associate_floatingip(
ctx.request_context, mac_address)
ctx.register_clean_up(callback)
return (floatingip, server)

def _skip(self, ctx, manager, software, **kwargs):
nova_inst = self._get_server_info(ctx, manager, software.hostedOn)
floatingip = nova_inst.get_floatingip()
result = None
if floatingip:
LOG.debug('Instance %s already has floatingip, skip', nova_inst.name)
result = floatingip, nova_inst
return result

def get_kwargs(self, ctx, manager, target, result):
params = {}
params['target'] = target
params['floatingip'], params['server'] = result
return params

def _get_server_info(self, ctx, manager, inst_key):
tmpl_inst = ctx.template.get_server(inst_key)
server = manager.get_server(ctx.request_context,
getattr(tmpl_inst, 'uuid', None),
getattr(tmpl_inst, 'name', None))
if not server:
raise RuntimeError('server %s no longer exist.' % inst_key)
return server[/mw_shl_code]

实现步骤的并发执行
前文提过对于互不依赖且在不同机器上的软件安装可以并发执行,清单 5 展示了如何通过 StepWrapper 的一个具体实现 ParallelStep 来完成这个功能。该实现有如下几个特点:

  • 借助了 eventlet 类库提供的 green thread 功能。Green thread 使用起来与传统的 thread 类似,只是实际上所有 green thread 都是通过一个传统的 thread 完成的。当一个 green thread 遇到 IO 操作需要等待时,会主动让步给其他 green thread 执行。
  • 继承了 MultiTargetStepMixin。这代表该步骤会应用于多个同类型的目标,例如对多个软件定义进行部署。如有需要,可以通过_get_targets 方法从原目标中解析出多个目标。
  • 继承了 StepWrapper。这代表该实现并不会涉及具体的业务逻辑操作,只是对其他步骤的一种修饰。假设用它来修饰软件部署的步骤,execute 方法中的传入的 target 是多个软件定义,那么对于每一个软件定义都会生成一个 green thread,并在该 green thread 中执行软件安装的步骤,达到并发执行的效果。

清单 5. The ParallelStep Class

[mw_shl_code=python,true]class MultiTargetStepMixin(object):
def _get_targets(self, ctx, fac, tgt, **kwargs):
return tgt

class ParallelStep(StepWrapper, MultiTargetStepMixin):
pool = eventlet.GreenPool(1000)

def execute(self, ctx, manager, target, **kwargs):
targets = self._get_targets(ctx, manager, target, **kwargs)
tasks = [self.pool.spawn(super(ParallelStep, self).execute,
ctx, manager, target, **kwargs)
for target in targets]
[task.wait() for task in tasks][/mw_shl_code]

实现部署流程的可配置性
要实现可配置性,就得有配置文件和解析配置文件的工具。为了提高开发效率,本文借助 OpenStack 社区提供的 oslo.config 类库来实现这一功能。

Oslo.config 的配置文件为 ini 格式。在清单 7 中,为 Step 类定义了一个新的属性 slug,可以看做是 Step 的别名。在清单 6 中,通过 Step 的别名来指代每个 Step。可以看到,整个部署流程由 init_deploy,create_stack,associate_floatingip,install_software 和 finish_deploy 这几个步骤组成。清单 7 还展示了解析配置文件的过程:

  • 将所有的 Step 都添加到全部变量 STEPS 当中,键值为每个 Step 的 slug。这里可以看到分配 Floating IP 和部署软件的步骤被组合到一起,并成为可并发部署的步骤。
  • 定义 get_step 方法,提供通过 slug 来获取 Step 对象的功能。
  • 在进行拓扑部署的时候,首先利用 CONF.topology.deploy_steps 获取到配置文件中定义的部署步骤序列,然后将它传给 get_step 方法得到对应的 step 对象。注意,这里的 Step 对象是一个 StepWrapper 对象,它包含了所有配置文件中定义的 Step。最后通过执行这个 Step 对象,就可以执行预定义的拓扑部署流程了。

清单 6. Deploy Flow Configuration

[mw_shl_code=text,true][topology]
deploy_steps =init_deploy, create_stack,associate_floatingip&install_software, finish_deploy[/mw_shl_code]

清单 7. Parsing Configuration
[mw_shl_code=python,true]class Step(object):
slug = None
...

# Register all steps.
STEPS = Steps()
for step in [InitializeDeploy(), CreateStack(),
PhaseExecutor(ParallelStep(AssociateFloatingip(), InstallSoftware())),
FinishDeploy(), InitUndeploy(), RemoveStack()]:
STEPS[step.slug] = step

def get_step(*slugs):
global STEPS
return StepWrapper(*[STEPS[slug] for slug in slugs])

class DeploymentManager(OpenstackHelperMixin):
...
pool = eventlet.GreenPool(1000)
actions = {}

def __init__(self):
self.actions['create'] = get_step(*CONF.topology.deploy_steps)
...

def create(self, request_ctx, deployment):
...
with DeploymentContext(
request_ctx, deployment, template, pkg_loader) as deploy_ctx:
step = self.actions['create']
step.execute(deploy_ctx, self, template)[/mw_shl_code]

扩展和可配置性
本节将展示如何在不修改原有代码的基础上,只通过增加新的 Python 模块和修改配置文件,来扩展拓扑部署流程和构建新的流程。

添加新的步骤
假设在拓扑部署的过程增加一个步骤,来检查用户申请的资源是否超过了他能持有的资源上限(当然这个功能不必增加新的步骤也可以实现,这里仅以此为例)。要实现这个功能,只需要新建一个 Python module,然后加入如清单 8 所示的代码。这里定义了一个名为 CheckUserQuota 的 StepImpl 实现,其_handle 方法包含具体的资源限制检查逻辑。然后将该实现注册到 STEPS 全局变量中。接着修改配置文件,将这个步骤放在创建基础设施之前,如清单 9 所示。这样就完成了步骤的添加,非常简单。

清单 8. Add New Step
[mw_shl_code=python,true]from step_definition import StepImpl, STEPS

class CheckUserQuota(StepImpl):
def _handle(self, ctx, manager, target, **kwargs):
...

STEPS[CheckUserQuota.slug] = CheckUserQuota[/mw_shl_code]

清单 9. Add CheckUserQuota Step
[mw_shl_code=text,true][topology]
deploy_steps =init_deploy, check_user_quota, create_stack,
                 associate_floatingip&install_software, finish_deploy[/mw_shl_code]

构建新的流程
有部署流程,就有销毁部署的流程,假设销毁部署的同时,需要在目标机器上卸载软件。那么构建这样一个流程,除了添加必要的新步骤外,可以重用原有的分配 Floating IP 步骤。在清单 10 中展示了定义新的步骤并将这些步骤添加到 STEPS 全局变量的过程,然后只需要读取清单 11 展示的配置信息,就可以在需要的时候执行销毁部署的流程了。

清单 10. Undeploy Step Definitions
[mw_shl_code=text,true]class InitUndeploy(StepImpl):
...

class RemoveStep(StepImpl):
...
class UninstallSoftware(StepImpl):
...

# Register new steps to ‘STEPS’ global variable.
STEPS[InitDeploy.slug] = InitDeploy
...

# Undeploy
get_step(CONF.topology.undeploy_steps).execute(...)[/mw_shl_code]

清单 11. Add Undeploy Configuration
[mw_shl_code=text,true][topology]
undeploy_steps =init_undeploy, associate_floatingip&uninstall_software, remove_stack[/mw_shl_code]

小结
本文所展示的拓扑部署流程实现,先将复杂的流程分解为多个功能单一的步骤(StepImpl),然后以配置文件定义步骤的执行顺序,最后用不同的方式(StepWrapper)将各个步骤组合起来,实现完整的部署流程。虽然实现过程中利用了 Python 语言的一些独有特性,但背后的设计思路用于其他编程语言同样可以实现,亦非常适用于解决其他复杂的流程问题。



没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条