OpenStack Keystone详解

Keystone 是 OpenStack 的核心组件之一,作为身份认证服务(Identity Service),它为整个 OpenStack 生态系统提供统一的身份验证、授权和服务目录管理。

三大核心功能:

  • 身份认证(Authentication):验证用户身份(如用户名 / 密码、令牌等)
  • 授权(Authorization):确定用户是否有权限执行特定操作
  • 服务目录(Service Catalog):管理 OpenStack 各服务的访问端点(Endpoint)

1、核心概念

1.1 User(用户)

代表一个使用 OpenStack 服务的个体、系统或服务。它拥有登录凭证(密码、API Key 等)。

1.2 Project / Tenant(项目 / 租户)

Project 用于将 OpenStack 的资源(计算、存储和网络)进行分组和隔离。

根据 OpenStack 服务的对象不同,Project 可以是一个客户(公有云,也叫租户)、部门或者项目组(私有云)。

注意:

  1. 资源的所有权是属于 Project 的,而不是 User。
  2. 每个 User(包括 admin)必须挂在 Project 里才能访问该 Project 的资源。 一个User可以属于多个 Project。
  3. admin 相当于 root 用户,具有最高权限

资源隔离的单元,用户必须属于某个项目才能操作资源(Tenant 是旧称,现在多称为 Project)

1.3 Role(角色)

权限的集合。它定义了一组用户可以执行的操作权限(如 member, admin, network-admin)。角色本身没有权限,其权限由各个 OpenStack 服务(Nova, Neutron等)在策略文件(policy.json)中定义。

示例:

json 复制代码
{   //用户需具有 admin角色 //
    "context_is_admin": "role:admin",
    //有管理员权限或请求用户的project_id和资源的project_id匹配 //
    "admin_or_owner": "is_admin:True or project_id:%(project_id)s",
    //检查用户上下文是否具有管理员权限。//
    "admin_api": "is_admin:True",
    //用户需具有 cloud_admin 角色 //
    "cloud_admin": "role:cloud_admin",

    // 引用规则admin_or_owner//
    "os_compute_api:servers:start": "rule:admin_or_owner",

    // 允许任何用户查看服务器元数据(metadata)
    "os_compute_api:servers:show_server_metadata": ""
}

1.4 Token(令牌)

一个加密字符串,是访问资源的凭证。用户认证后,Keystone 会颁发一个 Token。在后续调用其他服务 API 时,必须出示此 Token 以验证身份和权限。Token 有有效期。

1.5 Service(服务)

代表一个 OpenStack 服务,如 nova, neutron, cinder 等。在 Keystone 中注册

1.6 Endpoint(端点)

服务的访问地址(URL),分为 public(公网)、internal(内网)、admin(管理)三种类型一个服务对外暴露的可访问地址(URL)。

1.7 Catalog(服务目录)

所有 Service 和其 Endpoint 的列表。它在用户认证成功后随 Token 一起返回,告诉用户哪里可以访问所需的服务。

1.8 Domain(域)

项目、用户和组的集合。用于实现更高级别的隔离和管理隔离。一个域可以被认为是一个更大的组织或部门,其下包含多个项目和用户。默认有一个 Default 域。

1.9 Group(组)

用户的集合。可以给一个组分配角色,那么组内的所有用户都会继承这个角色在项目中的权限,简化了用户权限管理。

2、工作原理

sequenceDiagram actor User participant H as Horizon/CLI participant K as Keystone participant S as Other Service<br>(e.g., Nova) User->>H: 1. 提供用户名/密码<br>和项目名称 H->>K: 2. 认证请求<br>(用户名、密码、项目) K->>K: 3. 验证凭证<br>计算权限<br>生成Token<br>组装服务目录 K-->>H: 4. 返回认证响应<br>(Token & Service Catalog) H->>S: 5. 请求创建虚拟机<br>(携带Token) S->>K: 6. 验证Token有效性 K-->>S: 7. 返回Token详情<br>(用户、项目、角色) S->>S: 8. 根据策略(policy.json)<br>校验权限 S-->>H: 9. 执行操作并返回结果 H-->>User: 10. 显示操作结果

  1. 用户认证 (Authentication): 用户(通过 Horizon 或 CLI)向 Keystone 提供用户名、密码和要访问的项目名称。
  2. 验证与令牌颁发: Keystone 验证用户身份及其在该项目中的成员资格。验证通过后,Keystone 会:
    • 生成一个 Token
    • 根据用户的角色计算其权限。
    • 组装该用户可以访问的服务目录 (Catalog)
  3. 服务访问: 用户使用收到的 Token 去调用其他 OpenStack 服务(例如 Nova 创建虚拟机)。该请求中必须包含 X-Auth-Token 头信息。
  4. 令牌验证 (Validation): Nova 接收到请求后,无法自己识别 Token,它会向 Keystone 发出请求,询问"这个 Token 是否有效?它对应哪个用户和项目?"
  5. 授权决策 (Authorization): Keystone 验证 Token 并返回Token的详情(用户、项目、角色等)。Nova 然后根据自己的policy.json文件,判断该用户在该项目下拥有的角色是否有权执行创建虚拟机操作。
  6. 执行操作: 如果授权通过,Nova 执行操作;否则返回权限错误。

3、Keystone源码解析

Keystone 的源码主要位于 /keystone/ 目录下。

3.1 主要目录结构

  • keystone/common/: 通用工具(如配置、策略执行、上下文)
  • keystone/identity/: 身份后端驱动(SQL, LDAP)
  • keystone/assignment/: 资源(项目/域)和角色分配后端驱动
  • keystone/auth/: 认证插件(密码、令牌等)
  • keystone/token/: 令牌提供者(Fernet, UUID, PKI, PKIZ)
  • keystone/catalog/: 服务目录后端驱动
  • keystone/api/: 主要的 WSGI 路由和控制器(RESTful API 入口)
  • keystone/server/: 启动和部署相关的 Flask 应用配置

3.2 核心流程源码分析

3.2.1 密码认证与令牌颁发流程
  1. 请求入口:WSGI 路由与控制器
    文件keystone/auth/routers.py
python 复制代码
class AuthRouter(wsgi.ComposableRouter):
    def add_routes(self, mapper):
        # 注册认证路由
        mapper.connect('/auth/tokens',
                       controller=self._controllers['auth'],
                       action='authenticate_for_token',
                       conditions={'method': ['POST']})

当收到 POST /v3/auth/tokens请求时,路由将请求分发给 AuthController.authenticate_for_token方法。

  1. 认证控制器处理

文件路径: keystone/auth/controllers.py

python 复制代码
class AuthController(controller.V3Controller):
    def authenticate_for_token(self, context, auth_payload):
        # 1. 验证请求格式
        self._validate_authentication_request(context, auth_payload)
        
        # 2. 提取认证方法
        auth_methods = auth_payload['auth']['identity']['methods']
        auth_data = auth_payload['auth']['identity'].get('password', {})
        
        # 3. 选择认证插件
        auth_plugin = self._get_auth_plugin(auth_methods[0])
        
        # 4. 执行认证
        auth_context = auth_plugin.authenticate(context, **auth_data)
        
        # 5. 颁发令牌
        token_data = self.token_provider_api.issue_token(
            user_id=auth_context['user_id'],
            method_names=auth_methods,
            project_id=auth_context.get('project_id'),
            domain_id=auth_context.get('domain_id')
        )
        
        # 6. 构建响应
        response = wsgi.render_response(
            body=token_data,
            status=(201, 'Created')
        )
        response.headers['X-Subject-Token'] = token_data['token']['id']
        return response
  1. 密码认证插件实现
    文件路径: keystone/auth/plugins/password.py
python 复制代码
class Password(controller.AuthMethodHandler):
    def authenticate(self, context, user_id=None, password=None, 
                     user_domain_id=None, username=None):
        # 1. 获取用户域ID(如果未提供)
        if not user_domain_id:
            user_domain_id = CONF.identity.default_domain_id
        
        # 2. 获取用户对象
        try:
            if user_id:
                user_ref = self.identity_api.get_user(context, user_id)
            else:
                user_ref = self.identity_api.get_user_by_name(
                    context, username, user_domain_id)
        except exception.UserNotFound:
            raise exception.Unauthorized('Invalid user')
        
        # 3. 验证用户状态
        if not user_ref.get('enabled', True):
            raise exception.Unauthorized('User disabled')
        
        # 4. 验证密码
        try:
            self.identity_api.authenticate(
                context,
                user_id=user_ref['id'],
                password=password
            )
        except AssertionError:
            raise exception.Unauthorized('Invalid password')
        
        # 5. 返回认证上下文
        return {
            'user_id': user_ref['id'],
            'user_domain_id': user_domain_id,
            'project_id': self._get_project_id(context, auth_context),
            'domain_id': self._get_domain_id(context, auth_context)
        }
  1. 身份驱动验证密码
    文件路径: keystone/identity/drivers/sql.py
python 复制代码
class Identity(identity.Driver):
    def authenticate(self, context, user_id, password):
        # 1. 获取用户凭证
        try:
            cred_ref = self.get_credential(context, user_id)
        except exception.CredentialNotFound:
            raise AssertionError("Invalid credentials")
        
        # 2. 验证密码哈希
        if not self._check_password(password, cred_ref['blob']):
            raise AssertionError("Invalid credentials")
        
        # 3. 验证凭证状态
        if not cred_ref.get('enabled', True):
            raise AssertionError("Credential disabled")
  1. 令牌提供者颁发令牌
    文件路径: keystone/token/provider.py
python 复制代码
class TokenProviderAPI(object):
    def issue_token(self, user_id, method_names, **kwargs):
        # 1. 验证用户和项目/域
        self._assert_valid_user(user_id)
        self._assert_valid_project_or_domain(kwargs)
        
        # 2. 生成令牌ID
        token_id = self.token_formatter.create_token(
            user_id=user_id,
            **kwargs
        )
        
        # 3. 构建令牌数据
        token_data = {
            'token': {
                'methods': method_names,
                'expires_at': self._get_expiration_time(),
                'user': self._get_user_data(user_id),
                'project': self._get_project_data(kwargs.get('project_id')),
                'domain': self._get_domain_data(kwargs.get('domain_id')),
                'catalog': self._get_service_catalog(user_id, **kwargs)
            }
        }
        
        # 4. 持久化令牌
        self.token_provider.persist_token(token_id, token_data)
        
        return token_data
  1. Fernet 令牌驱动实现
    文件路径: keystone/token/providers/fernet.py
python 复制代码
class TokenFormatter(object):
    def create_token(self, user_id, project_id=None, domain_id=None):
        # 1. 准备令牌载荷
        payload = {
            'exp': int(time.time()) + CONF.token.expiration,
            'iat': int(time.time()),
            'user': {'id': user_id},
            'aud': self._get_audience(),
            'iss': CONF.token.issuer
        }
        
        if project_id:
            payload['project'] = {'id': project_id}
        elif domain_id:
            payload['domain'] = {'id': domain_id}
        
        # 2. 序列化载荷
        serialized_payload = msgpack.packb(payload)
        
        # 3. 使用Fernet加密
        fernet = Fernet(CONF.fernet_tokens.key_repository)
        token = fernet.encrypt(serialized_payload)
        
        return token.decode('utf-8')
3.2.2 令牌验证流程

其他服务调用 GET /v3/auth/tokens

  1. 请求入口:路由与控制器
    文件路径: keystone/auth/routers.py
python 复制代码
class AuthRouter(wsgi.ComposableRouter):
    def add_routes(self, mapper):
        # 注册令牌验证路由
        mapper.connect('/auth/tokens',
                       controller=self._controllers['auth'],
                       action='validate_token',
                       conditions={'method': ['GET']})
  1. 令牌验证控制器
    文件路径: keystone/auth/controllers.py
python 复制代码
class AuthController(controller.V3Controller):
    def validate_token(self, context):
        # 1. 获取待验证的令牌ID
        token_id = self._get_subject_token_id(context)
        
        # 2. 验证请求者权限
        self.assert_authenticated()
        
        # 3. 调用令牌提供者API进行验证
        token_data = self.token_provider_api.validate_token(token_id)
        
        # 4. 构建响应
        return wsgi.render_response(body=token_data)
  1. 令牌提供者 API 实现
    文件路径: keystone/token/provider.py
python 复制代码
class TokenProviderAPI(object):
    def validate_token(self, token_id):
        # 1. 检查令牌格式
        if not self.token_formatter.validate_token(token_id):
            raise exception.ValidationError(_('Invalid token format'))
        
        # 2. 从缓存或持久化存储获取令牌数据
        token_data = self._get_token_data(token_id)
        
        # 3. 验证令牌状态
        self._validate_token_state(token_data)
        
        # 4. 验证令牌有效期
        self._validate_token_expiration(token_data)
        
        # 5. 验证用户状态
        self._assert_valid_user(token_data['token']['user']['id'])
        
        # 6. 验证项目/域状态
        if 'project' in token_data['token']:
            self._assert_valid_project(token_data['token']['project']['id'])
        elif 'domain' in token_data['token']:
            self._assert_valid_domain(token_data['token']['domain']['id'])
        
        # 7. 构建响应数据
        return self._format_token_response(token_data)
  1. 令牌数据获取逻辑
    文件路径: keystone/token/provider.py
python 复制代码
class TokenProviderAPI(object):
    def _get_token_data(self, token_id):
        # 1. 检查缓存
        token_data = self.token_cache.get_token(token_id)
        if token_data:
            return token_data
        
        # 2. 从持久化存储获取
        token_data = self.token_driver.get_token_data(token_id)
        if not token_data:
            raise exception.TokenNotFound(_('Token not found'))
        
        # 3. 验证令牌签名
        if not self.token_formatter.validate_token_signature(token_id):
            raise exception.Unauthorized(_('Invalid token signature'))
        
        # 4. 缓存结果
        self.token_cache.set_token(token_id, token_data)
        
        return token_data
  1. Fernet 令牌验证实现
    文件路径: keystone/token/providers/fernet.py
python 复制代码
class TokenFormatter(object):
    def validate_token(self, token_id):
        # 1. 基本格式检查
        if not token_id or len(token_id) < CONF.fernet_tokens.min_token_size:
            return False
        
        # 2. Fernet 令牌格式检查
        return token_id.count('-') == 3 and len(token_id.split('-')[0]) == 8
    
    def validate_token_signature(self, token_id):
        # 1. 尝试使用所有可用密钥解密
        for key in self._get_valid_keys():
            try:
                fernet = Fernet(key)
                payload = fernet.decrypt(token_id.encode('utf-8'))
                msgpack.unpackb(payload)  # 验证载荷格式
                return True
            except (cryptography.fernet.InvalidToken, msgpack.UnpackException):
                continue
        
        return False
    
    def _get_valid_keys(self):
        """获取所有有效的加密密钥"""
        keys = []
        for key_repo in CONF.fernet_tokens.key_repository:
            for key_file in os.listdir(key_repo):
                if key_file.endswith('.key'):
                    with open(os.path.join(key_repo, key_file), 'rb') as f:
                        keys.append(f.read().strip())
        return keys
  1. 令牌缓存实现
    文件路径: keystone/token/providers/common.py
python 复制代码
class TokenCache(object):
    def __init__(self):
        self._cache = {}
        self._lock = threading.Lock()
    
    def get_token(self, token_id):
        with self._lock:
            # 1. 检查内存缓存
            if token_id in self._cache:
                entry = self._cache[token_id]
                if time.time() < entry['exp']:
                    return entry['data']
                del self._cache[token_id]
            
            # 2. 检查外部缓存(如 Memcached)
            if CONF.token.caching and CONF.cache.enabled:
                cache_key = f"token_{token_id}"
                cached = cache.get(cache_key)
                if cached and time.time() < cached['exp']:
                    self._cache[token_id] = cached  # 填充内存缓存
                    return cached['data']
        
        return None
    
    def set_token(self, token_id, token_data):
        with self._lock:
            # 1. 设置内存缓存
            entry = {
                'data': token_data,
                'exp': time.time() + CONF.token.cache_time
            }
            self._cache[token_id] = entry
            
            # 2. 设置外部缓存
            if CONF.token.caching and CONF.cache.enabled:
                cache_key = f"token_{token_id}"
                cache.set(cache_key, entry, CONF.token.cache_time)
3.2.3 服务注册
  1. 服务注册 API 入口
    文件路径: keystone/service/routers.py
python 复制代码
class ServiceRouter(wsgi.ComposableRouter):
    def add_routes(self, mapper):
        # 服务注册路由
        mapper.connect('/services',
                       controller=self._controllers['service'],
                       action='create_service',
                       conditions={'method': ['POST']})
        
        # 端点注册路由
        mapper.connect('/endpoints',
                       controller=self._controllers['endpoint'],
                       action='create_endpoint',
                       conditions={'method': ['POST']})
  1. 服务控制器实现
    文件路径: keystone/service/controllers.py
python 复制代码
class ServiceController(controller.V3Controller):
    @controller.protected()
    def create_service(self, context, service):
        """创建服务实体"""
        # 1. 验证输入数据
        validation.lazy_validate(schema.service_create, service)
        service_ref = service['service']
        
        # 2. 生成服务ID
        service_id = service_ref.get('id') or uuid.uuid4().hex
        
        # 3. 提取服务属性
        service_type = service_ref['type']
        service_name = service_ref.get('name', service_type)
        description = service_ref.get('description', '')
        enabled = service_ref.get('enabled', True)
        
        # 4. 调用服务API创建服务
        service_dict = self.catalog_api.create_service(
            service_id,
            {'name': service_name, 'type': service_type,
             'description': description, 'enabled': enabled}
        )
        
        # 5. 返回创建的服务信息
        return {'service': service_dict}
  1. 端点控制器实现
    文件路径: keystone/service/controllers.py
python 复制代码
class EndpointController(controller.V3Controller):
    @controller.protected()
    def create_endpoint(self, context, endpoint):
        """创建服务端点"""
        # 1. 验证输入数据
        validation.lazy_validate(schema.endpoint_create, endpoint)
        endpoint_ref = endpoint['endpoint']
        
        # 2. 生成端点ID
        endpoint_id = endpoint_ref.get('id') or uuid.uuid4().hex
        
        # 3. 提取端点属性
        service_id = endpoint_ref['service_id']
        interface = endpoint_ref['interface']  # public, admin, internal
        url = endpoint_ref['url']
        region_id = endpoint_ref.get('region_id')
        enabled = endpoint_ref.get('enabled', True)
        
        # 4. 验证服务是否存在
        self.catalog_api.get_service(service_id)
        
        # 5. 调用端点API创建端点
        endpoint_dict = self.catalog_api.create_endpoint(
            endpoint_id,
            {'service_id': service_id, 'interface': interface,
             'url': url, 'region_id': region_id, 'enabled': enabled}
        )
        
        # 6. 返回创建的端点信息
        return {'endpoint': endpoint_dict}
  1. 目录服务 API 实现
    文件路径: keystone/catalog/core.py
python 复制代码
class CatalogManager(object):
    def __init__(self):
        # 加载后端驱动
        self.driver = self._get_driver()
        
    def _get_driver(self):
        # 根据配置选择驱动
        if CONF.catalog.driver == 'sql':
            from keystone.catalog.backends import sql
            return sql.Catalog()
        elif CONF.catalog.driver == 'templated':
            from keystone.catalog.backends import templated
            return templated.Catalog()
        else:
            raise exception.ConfigError('Invalid catalog driver')
    
    def create_service(self, service_id, service):
        """创建服务实体"""
        # 验证服务类型
        self._validate_service_type(service['type'])
        
        # 调用驱动创建服务
        return self.driver.create_service(service_id, service)
    
    def create_endpoint(self, endpoint_id, endpoint):
        """创建服务端点"""
        # 验证接口类型
        self._validate_interface(endpoint['interface'])
        
        # 调用驱动创建端点
        return self.driver.create_endpoint(endpoint_id, endpoint)
  1. SQL 后端驱动实现
    文件路径: keystone/catalog/backends/sql.py
python 复制代码
class Catalog(DriverBase):
    def create_service(self, service_id, service):
        """在SQL数据库中创建服务"""
        with sql.session_for_write() as session:
            # 检查服务类型是否唯一
            if session.query(models.Service).filter_by(
                type=service['type']).first():
                raise exception.Conflict('Service type already exists')
            
            # 创建服务对象
            service_ref = models.Service()
            service_ref.update({
                'id': service_id,
                'name': service['name'],
                'type': service['type'],
                'description': service.get('description'),
                'enabled': service.get('enabled', True)
            })
            
            # 添加到会话并提交
            session.add(service_ref)
            session.flush()
            
            return service_ref.to_dict()
    
    def create_endpoint(self, endpoint_id, endpoint):
        """在SQL数据库中创建端点"""
        with sql.session_for_write() as session:
            # 检查服务是否存在
            service_ref = session.query(models.Service).get(
                endpoint['service_id'])
            if not service_ref:
                raise exception.ServiceNotFound(
                    service_id=endpoint['service_id'])
            
            # 检查接口类型是否重复
            if session.query(models.Endpoint).filter_by(
                service_id=endpoint['service_id'],
                interface=endpoint['interface']).first():
                raise exception.Conflict(
                    'Endpoint interface already exists for this service')
            
            # 创建端点对象
            endpoint_ref = models.Endpoint()
            endpoint_ref.update({
                'id': endpoint_id,
                'service_id': endpoint['service_id'],
                'interface': endpoint['interface'],
                'url': endpoint['url'],
                'region_id': endpoint.get('region_id'),
                'enabled': endpoint.get('enabled', True)
            })
            
            # 添加到会话并提交
            session.add(endpoint_ref)
            session.flush()
            
            return endpoint_ref.to_dict()
  1. 模板化后端驱动实现
    文件路径: keystone/catalog/backends/templated.py
python 复制代码
class Catalog(DriverBase):
    def __init__(self):
        # 加载配置文件
        self.conf = self._load_config()
        
    def _load_config(self):
        """从配置文件加载服务目录模板"""
        with open(CONF.catalog.template_file, 'r') as f:
            return json.load(f)
    
    def create_service(self, service_id, service):
        """模板驱动不支持动态创建服务"""
        raise exception.NotImplemented(
            'Dynamic service creation not supported in templated driver')
    
    def create_endpoint(self, endpoint_id, endpoint):
        """模板驱动不支持动态创建端点"""
        raise exception.NotImplemented(
            'Dynamic endpoint creation not supported in templated driver')
    
    def get_catalog(self, user_id, project_id):
        """从模板生成服务目录"""
        # 替换模板中的变量
        catalog = copy.deepcopy(self.conf['catalog'])
        for service in catalog:
            for endpoint in service['endpoints']:
                endpoint['url'] = endpoint['url'].replace(
                    '$HOST', CONF.default_public_host)
        return catalog

4、常用命令

4.1 身份(Identity)管理

1. 身份(Identity)管理

  • 创建用户:

    bash 复制代码
    openstack user create --password-prompt --email user@example.com --domain Default myuser
    • --password-prompt 会交互式让你输入密码。
    • --domain 指定用户所在的域。
  • 列出用户:

    bash 复制代码
    openstack user list
  • 创建用户组 (用于LDAP集成或批量分配角色):

    bash 复制代码
    openstack group create --description "Developers Group" developers
  • 将用户加入用户组:

    bash 复制代码
    openstack group add user developers myuser

4.2 资源(Resource)与分配(Assignment)管理

  • 创建项目(Project):

    bash 复制代码
    openstack project create --description "Demo Project" --domain Default demoproject
  • 创建角色(Role):

    bash 复制代码
    openstack role create myrole
    • 通常内置 admin, _member_, reader 等角色。
  • 给用户在项目上授予角色 (核心操作):

    bash 复制代码
    openstack role add --project demoproject --user myuser myrole
    • 这条命令意味着 myuserdemoproject 中拥有 myrole 的权限。
  • 检查角色分配:

    bash 复制代码
    openstack role assignment list --user myuser --project demoproject --names

4.3 服务目录(Catalog)管理

  • 注册服务 (如注册Glance镜像服务):

    bash 复制代码
    openstack service create --name glance --description "OpenStack Image" image
  • 创建服务的端点(Endpoint):

    bash 复制代码
    openstack endpoint create --region RegionOne image public http://glance-host:9292
    openstack endpoint create --region RegionOne image internal http://glance-host:9292
    openstack endpoint create --region RegionOne image admin http://glance-host:9292
  • 列出所有服务和端点:

    bash 复制代码
    openstack service list
    openstack endpoint list

4.4 令牌(Token)操作

  • 作为特定用户获取令牌(调试神器) :

    bash 复制代码
    openstack --os-username myuser --os-password mypassword --os-project-name demoproject token issue
    • 这会返回一个详细的令牌信息,包括过期时间、授权范围等,用于调试权限问题。

4.5 域(Domain)管理

  • 创建域:

    bash 复制代码
    openstack domain create --description "My Company" mycompany
  • 在特定域中创建用户:

    bash 复制代码
    openstack user create --email john@mycompany.com --domain mycompany john
相关推荐
安全菜鸟9 天前
传统方式部署OpenStack具体教程
openstack
哈里谢顿2 个月前
Ironic 中 Clean/deploy Step 延迟执行的原因分析
openstack
哈里谢顿2 个月前
ironic中为什么 IPMI Hardware Type 必须支持 IPMIManagement
openstack
哈里谢顿2 个月前
Ironic 中各个接口的作用详解
openstack
CZIDC2 个月前
博客摘录「 华为云平台-FusionSphere OpenStack 8.2.1 系统加固」2025年7月15日
linux·服务器·笔记·华为云·openstack
行止62 个月前
OpenStack云平台管理
linux·openstack
曼汐 .2 个月前
私有云平台实战-OpenStack
openstack
哈里谢顿2 个月前
python的Mixin设计模式学习,以ironic-python-agent代码为例
openstack
曼汐 .2 个月前
私有云平台实战-OpenStack入门体验
openstack