OpenStack nova修改云主机密码功能及云主机内外部启动时间不一致问题




云主机内外部启动时间不一致问题

最近看了一个问题,症状是云主机过一段时间就无法登陆,怀疑是云主机自动关机或者其他异常,就看了下qemu日志,发现里面有如下日志:

2017-12-19 19:01:28.460+0000: shutting down
2017-12-20 19:01:53.809+0000: shutting down
2017-12-21 19:01:27.800+0000: shutting down
2017-12-22 19:01:59.231+0000: shutting down
2017-12-23 19:01:38.107+0000: shutting down

看到这段,以之前的经验判断是云主机被关机了,但感觉很奇怪,这个时间点是在凌晨3点(日志中时间是UTC)平台都没人登录,肯定不是人为操作,刚开始想了半天没明白,于是登录到云主机中查看uptime,看到已经启动了好几周了:

[root@jira ~]# uptime -p
up 5 weeks, 4 days, 14 hours, 55 minutes

物理机上看qemu进程启动时间:

[root@vs-controller qemu]# ps -ef| grep 70740314
qemu      76239      1 18 Dec24 ?        06:28:37 /usr/libexec/qemu-kvm -name 70740314-570e-4190-9be7-43e2eb2ae883 -S -machine pc-i440fx-rhel7.2.0,accel=kvm,usb=off -cpu Haswell-noTSX,+abm,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme -m size=8388608k,slots=16,maxmem=33554432k -realtime mlock=off -smp 4,maxcpus=8,sockets=1,cores=1,threads=8 -numa node,nodeid=0,cpus=0-7,mem=8192 -uuid 70740314-570e-4190-9be7-43e2eb2ae883

[root@vs-controller qemu]# ps -eo pid,lstart,etime | grep 76239
 76239 Sun Dec 24 05:53:22 2017  1-10:56:12

居然是昨天启动的,就更奇怪了。

看了下nova-compute日志,发现这个时间点是在做备份(nova backup接口),配置的是cold snapshot方式(备份代码流程可参考之前写的这篇文章,以及这篇文章),所以备份过程中会把云主机休眠,于是明白了内部uptime看到的启动时间和外部qemu进程启动时间不一致的原因,休眠并不影响云主机内部时间,休眠唤醒过程中云主机内部并不会执行关机开机流程,云主机的进程状态、系统状态都不发生变化(CPU寄存器、内存数据都不变),类似经历了一次热迁移操作。

手工实验了一下,流程如下:

  1. 关闭openstack-nova-compute服务,防止通过virsh命令休眠云主机后云主机状态被同步成关机状态
  2. 通过vnc客户端登陆到云主机内部,查看uptime,已经启动了好几天
  3. virsh managedsave UUID
  4. ps查看qemu进程,已经不存在
  5. 等待几分钟,执行virsh start UUID
  6. 再次通过vnc客户端登陆云主机内部,查看uptime,发现启动时间还是好几天
  7. ps查看qemu进程启动时间,发现是刚刚启动
  8. date查看云主机内部时间,发现仍然停留在休眠时间点,也就是几分钟之前,所以如果涉及到休眠操作,建议使用ntp服务同步云主机内部时间

修改云主机密码功能

另外为了登录到云主机内部,顺便看了下修改密码功能,之前我们用H版本的时候,是自己修改的nova代码实现的修改密码功能,调用qemu guest agent的guest-set-user-password接口,只支持修改root(Linux系统)和Administrator(Windows系统)两个账户的密码。看了下M版本的nova相关代码流程,已经提供了相关接口,nova libvirt driver的实现是直接调用python libvirt API,

    def _can_set_admin_password(self, image_meta):
        if (CONF.libvirt.virt_type not in ('kvm', 'qemu') or
            not self._host.has_min_version(MIN_LIBVIRT_SET_ADMIN_PASSWD)):
            raise exception.SetAdminPasswdNotSupported()

        hw_qga = image_meta.properties.get('hw_qemu_guest_agent', '')
        if not strutils.bool_from_string(hw_qga):
            raise exception.QemuGuestAgentNotEnabled()

    def set_admin_password(self, instance, new_pass):
        self._can_set_admin_password(instance.image_meta)

        guest = self._host.get_guest(instance)
        user = instance.image_meta.properties.get("os_admin_user")
        if not user:
            if instance.os_type == "windows":
                user = "Administrator"
            else:
                user = "root"
        try:
            guest.set_user_password(user, new_pass)
        except libvirt.libvirtError as ex:
            error_code = ex.get_error_code()
            msg = (_('Error from libvirt while set password for username '
                     '"%(user)s": [Error Code %(error_code)s] %(ex)s')
                   % {'user': user, 'error_code': error_code, 'ex': ex})
            raise exception.NovaException(msg)

libvirt的最低版本要求是2.0.0,我们用的正好满足。

官方实现还支持自定义修改的用户名,在镜像property里面设置即可,比如–property os_admin_user=admin。

    def set_user_password(self, user, new_pass):
        """Configures a new user password."""
        self._domain.setUserPassword(user, new_pass, 0)  ## 调用libvirt接口

对应的libvirt virsh命令是(至少2.0.0版本):

[root@vs-controller ~]# virsh set-user-password bbf6d1b8-50fd-45e4-bd48-2a7701b5bef6 root qwerty
Password set successfully for root in bbf6d1b8-50fd-45e4-bd48-2a7701b5bef6

看了下libvirt源码,qemu driver的实现是调用的qemu guest agent接口,

static int
qemuDomainSetUserPassword(virDomainPtr dom,
                          const char *user,
                          const char *password,
                          unsigned int flags)
{
    virQEMUDriverPtr driver = dom->conn->privateData;
    virDomainObjPtr vm;
    qemuAgentPtr agent;
    int ret = -1;
    int rv;

    virCheckFlags(VIR_DOMAIN_PASSWORD_ENCRYPTED, -1);

    if (!(vm = qemuDomObjFromDomain(dom)))
        return ret;

    if (virDomainSetUserPasswordEnsureACL(dom->conn, vm->def) < 0)
        goto cleanup;

    if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0)
        goto cleanup;

    if (!virDomainObjIsActive(vm)) {
        virReportError(VIR_ERR_OPERATION_INVALID,
                       "%s", _("domain is not running"));
        goto endjob;
    }

    if (!qemuDomainAgentAvailable(vm, true))   // qemu agent不可用就退出
        goto endjob;

    agent = qemuDomainObjEnterAgent(vm);
    rv = qemuAgentSetUserPassword(agent, user, password,
                                  flags & VIR_DOMAIN_PASSWORD_ENCRYPTED);
    qemuDomainObjExitAgent(vm, agent);

    if (rv < 0)
        goto endjob;

    ret = 0;

 endjob:
    qemuDomainObjEndJob(driver, vm);

 cleanup:
    virDomainObjEndAPI(&vm);
    return ret;
}

与我们在H版本自己写代码实现的底层接口是一样的,只是低版本的libvirt没有提供api,是在libvirt agent-command模块实现的(python-libvirt包里面提供了libvirt_qemu模块,可以import进来调用qemu guest agent接口),对应的命令行是:

$ echo -n "123456" | base64
MTIzNDU2
$ virsh -c qemu:///system  qemu-agent-command f21x86_64 \
   '{ "execute": "guest-set-user-password",
      "arguments": { "crypted": false,
                     "username": "root",
                     "password": "MTIzNDU2" } }'
  {"return":{}}

# Or a password that has already been run though a crypt(3) like
# algorithm appropriate for the guest, again then base64 encoded:

$ echo -n '$6$n01A2Tau$e...snip...DfMOP7of9AJ1I8q0' | base64
JDYkb...snip...YT2Ey

$ virsh -c qemu:///system  qemu-agent-command f21x86_64 \
   '{ "execute": "guest-set-user-password",
      "arguments": { "crypted": true,
                     "username": "root",
                     "password": "JDYkb...snip...YT2Ey" } }'