2025 OSSCA OpenStack SDK/CLI Challengers

SangYeon Min·2025년 7월 22일

STUDY-OPENSTACK

목록 보기
5/5
post-thumbnail

1주차

Openstack 이란

  • Cloud Infrastructure for Virtual Machines, Bare Metals and Containers
  • 개인적인 클라우드를 만드는 것을 목적으로 한다

Cloud Infrastructure?

  • 물리적으로 제공되던 IT 인프라 장비들을 가상화하여 API로 제어할 수 있도록 제공하는 것
  • 네트워크 - SDN, 서버 - 인스턴스, 스토리지 - Block/Object Storage 이미지 - 기존 설치하던 OS를 이미지화

Core Components

  • Nova, Neutron, Glance, Cinder, Swift, Keystone
  • Designate (DNS Service) / Barbican (Key Manager)

Openstack 개발

  • 코드 리뷰 (Gerrit) / CI 도구 (ZuuL)
  • 코드 머지는 Core Contributor의 +2 점수를 받으면 머지가 가능

https://review.opendev.org/

이슈 관리

  • launchpad를 사용하다가 최근 Story Board로 이전 중

회의록/의견 공유 (Etherpad)

  • OpenStack 커뮤니티에서 논의되는 모든 내용은 etherpad에 기록 및 공유
  • 아카이빙이 잘 되어 있다

Project Team Gathering

  • 6개월마다 OpenStack 업스트림 개발자들이 팀별로 모임, 최근에는 온라인
  • 3 Controller - 3 Compute가 일반적인 OpenStack의 구성으로 이는 다중화를 위해서이다
  • SAN: 저장소용 백엔드 서버 / SAF: 분산 스토리지
  • 결국 OpenStack과 같은 IaaS, PaaS 클라우드 기술은 인스턴스 (컴퓨팅 리소스)를 제공하기 위한 것이며, 따라서 가장 메인이 되는 프로젝트는 Nova, Neutron이 된다

2주차

python-openstackcli

  • OpenStack 클라우드의 서비스들에 대한 CLI 도구
  • python-openstackcli를 통해 다양한 서비스 관리 및 조작 가능
  • 다양한 OpenStack 서비스들에 대한 통합 CLI 제공
  • Kubernetes에서의 kubectl와 유사한 느낌
  • nova list -> openstack server list와 같이 활용 가능하도록 함
  • 최종 목표는 Barbican에 대한 명령 집합을 추가하는 것

https://opendev.org/openstack/python-openstackclient

setup.cfg

  • setup.cfg 파일은 Python 프로젝트의 메타데이터와 구성 옵션을 정의하는 파일
  • 최근 해당 패키징의 내용이 Deprecated되어 현재는 사용되지 않는다

/python-openstackcli/openstackclient/compute/v2/server.py

class ListServer(command.Lister):
    _description = _("List servers")

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        parser.add_argument(
            '--reservation-id',
            metavar='<reservation-id>',
            help=_('Only return instances that match the reservation'),
        )
        parser.add_argument(
            '--ip',
            metavar='<ip-address-regex>',
            help=_('Regular expression to match IP addresses'),
        )
        parser.add_argument(
            '--ip6',
            metavar='<ip-address-regex>',
            help=_(
                'Regular expression to match IPv6 addresses. Note '
                'that this option only applies for non-admin users '
                'when using ``--os-compute-api-version`` 2.5 or greater.'
            ),
        )
        ...
        parser.add_argument(
            '--status',
            metavar='<status>',
            choices=(
                'ACTIVE',
                'BUILD',
                'DELETED',
                'ERROR',
                'HARD_REBOOT',
                'MIGRATING',
                'PASSWORD',
                'PAUSED',
                'REBOOT',
                'REBUILD',
                'RESCUE',
                'RESIZE',
                'REVERT_RESIZE',
                'SHELVED',
                'SHELVED_OFFLOADED',
                'SHUTOFF',
                'SOFT_DELETED',
                'SUSPENDED',
                'VERIFY_RESIZE',
            ),
            help=_('Search by server status'),
        )
        ...
  • 예시로 위 클래스를 볼 수 있는데 이는 openstack server list를 쳤을때 실행되는 클래스이다
  • 또한 openstackcli 프로젝트의 다른 내용들은 모두 포맷이 동일하다
    • get_parser: Option들을 파싱해서 처리하는 메서드
    • take_action:
      • get_parser를 통해 만들어진 데이터를 통해 실제 동작을 수행하는 메서드
      • SDK에서 만들어진 인터페이스를 호출하는 역할
      • 대체로 해당 부분을 많이 볼 예정이다

take_action까지의 Flow

openstack server list 분석

--debug 옵션은 실행한 CLI가 결과값을 보여주기까지 어떤 API를 호출하고 결과값을 가져오는지의 과정을 보여주는 옵션이다.

위 옵션을 통해서 openstackcli 명령을 분석할 수 있다.

START with options: server list --debug
options: Namespace(verbose_level=3, log_file=None, deferred_help=False, debug=True, cloud='', region_name='', cacert=None, cert='', key='', verify=None, insecure=None, default_domain='default' ...
Auth plugin password selected
...
{
   "endpoints":[
      {
         "id":"5882de4449b24b73a2b11835df9db71d",
         "interface":"public",
         "region_id":"RegionOne",
         "url":"http://controller:9292",
         "region":"RegionOne"
      },
      {
         "id":"bc020a0587104bff898dc0dcd41ca0ed",
         "interface":"admin",
         "region_id":"RegionOne",
         "url":"http://controller:9292",
         "region":"RegionOne"
      },
      {
         "id":"e18153d6e3924d268112ea67d1544f42",
         "interface":"internal",
         "region_id":"RegionOne",
         "url":"http://controller:9292",
         "region":"RegionOne"
      }
   ],
   "id":"6048f1fd11cb4dcda5de41d1251a0296",
   "type":"image",
   "name":"glance"
},
...
  • 이러한 server list를 만들기 위해서 토큰을 각자 발급받아서 API로 넘겨주고 take_action에서 촤종 출력 결과인 표를 만들어주는 형태이다
  • Keystone이 각 엔드포인트들을 받아와주고, CLI가 이를 활용해 다른 서비스들을 호출해준다
    • 이때 각 엔드포인트는 VIP를 사용한다
    • 만약 해당 VIP가 변경되면 openstack endpoint set ...을 통해 변경한다
    • 이를 수정한 순간 Keystone에서 받아올 수 있다
    • 실제 Product 환경에서는 IP로 서비스를 등록하지 않고, 도메인이나 호스트 네임으로 등록한다
  • Controller Node가 여러개인 경우 (HA 구성을 통해) VIP로 Controller Node들을 묶어서 하나처럼 사용한다.
    즉, 하나의 서비스에 단 하나의 엔드포인트만이 존재한다
REQ: curl -g -i -X GET http://controller:8774/v2.1/servers/detail -H "Accept: application/json" -H "User-Agent: python-novaclient" -H "X-Auth-Token: {SHA256}11..." -H "X-OpenStack-Nova-API-Version: 2.1"
Starting new HTTP connection (1): controller:8774
...
RESP BODY:
{
   "servers":[
      {
         "id":"b4ce6073-3d50-4c06-b284-c6e69e1945b8",
         "name":"test-instance",
         "status":"ACTIVE",
         "tenant_id":"0636a6ee7b2242e7ba9faf5e7d1daed4",
         "user_id":"12529d0a6bfa4cb1894172f0fe354de9",
         "metadata":{
            
         },
         "hostId":"ccbe882631a17eb2fcc5b981a33cd66fcf719bee59d1c6c96e741582",
         "image":{
            "id":"73c6e7ee-3b6c-40dd-bdc5-96058e183faa",
            "links":[
               {
                  "rel":"bookmark",
                  "href":"http://controller:8774/images/73c6e7ee-3b6c-40dd-bdc5-96058e183faa"
               }
            ]
         },
         "flavor":{
            "id":"c4ee93fc-2e98-4473-aab7-ff75214be0c0",
            "links":[
               {
                  "rel":"bookmark",
                  "href":"http://controller:8774/flavors/c4ee93fc-2e98-4473-aab7-ff75214be0c0"
               }
            ]
         },
         "created":"2025-07-10T13:20:30Z",
         "updated":"2025-07-21T09:06:11Z",
         "addresses":{
            "private-net":[
               {
                  "version":4,
                  "addr":"10.0.0.77",
                  "OS-EXT-IPS:type":"fixed",
                  "OS-EXT-IPS-MAC:mac_addr":"fa:16:3e:20:f8:0d"
               }
            ],
            "ext-net":[
               {
                  "version":4,
                  "addr":"192.168.100.73",
                  "OS-EXT-IPS:type":"fixed",
                  "OS-EXT-IPS-MAC:mac_addr":"fa:16:3e:64:23:06"
               }
            ]
         },
         "accessIPv4":"",
         "accessIPv6":"",
         "links":[
            {
               "rel":"self",
               "href":"http://controller:8774/v2.1/servers/b4ce6073-3d50-4c06-b284-c6e69e1945b8"
            },
            {
               "rel":"bookmark",
               "href":"http://controller:8774/servers/b4ce6073-3d50-4c06-b284-c6e69e1945b8"
            }
         ],
         "OS-DCF:diskConfig":"MANUAL",
         "progress":0,
         "OS-EXT-AZ:availability_zone":"nova",
         "config_drive":"",
         "key_name":"mykey",
         "OS-SRV-USG:launched_at":"2025-07-10T13:20:36.000000",
         "OS-SRV-USG:terminated_at":null,
         "OS-EXT-SRV-ATTR:host":"compute",
         "OS-EXT-SRV-ATTR:instance_name":"instance-0000001c",
         "OS-EXT-SRV-ATTR:hypervisor_hostname":"compute",
         "OS-EXT-STS:task_state":null,
         "OS-EXT-STS:vm_state":"active",
         "OS-EXT-STS:power_state":1,
         "os-extended-volumes:volumes_attached":[
            
         ],
         "security_groups":[
            {
               "name":"default"
            },
            {
               "name":"default"
            }
         ]
      }
   ]
}
  • python-openstackcli는 curl을 통해 Token을 받아온 뒤 이를 통해 nova와 통신하는 예시는 위와 같다

Openstack API 명세서

위와 같은 명세서를 기반으로 Req에 대한 Res를 받아볼 수 있다.

openstacksdk

  • OpenStack SDK는 개발자가 복잡한 REST API를 몰라도 클라우드 자원을 다룰 수 있게 해주는 강력한 도구
  • OpenStack SDK는 역할이 명확히 구분된 세 가지 계층(Connection, Proxy, Resource)으로 구성

https://docs.openstack.org/openstacksdk
https://opendev.org/openstack/openstacksdk

Connection: 통합 진입점

from openstack import connection

# 인증 정보를 담아 Connection 객체 생성
conn = connection.Connection(
    region_name='RegionOne',
    auth={
        'auth_url': 'http://<엔드포인트>/identity',
        'username': 'admin',
        'password': '<PW>',
        'project_id': '67924c35cf3c4362b6f49e7be59492de',
        'user_domain_id': 'default'
    },
    compute_api_version='2',
)
  • SDK 사용의 시작점
  • 인증 정보를 관리하고 모든 OpenStack 서비스에 접근할 수 있는 관문 역할을 함
  • conn 객체 하나로 모든 서비스(compute, network 등)를 제어할 수 있음

Proxy: 서비스 액션 계층

# openstack/compute/v2/_proxy.py
from openstack.compute.v2 import server as _server
...
def servers(self, details=True, **query):
    if all_projects:
        query['all_projects'] = True
base_path = '/servers/detail' if details else None
    return self._list(_server.Server, base_path=base_path, **query)
  • 개발자가 실제로 상호작용하는 인터페이스 conn.compute.servers()처럼 사용하기 쉬운 메소드를 제공
  • 서버 생성, 조회, 삭제 등 실제 작업을 처리

Resource: 데이터 모델 계층

# openstack/compute/v2/server.py
from openstack import resource
...
class Server(resource.Resource, ...):
    resource_key = 'server'
    resources_key = 'servers'
    base_path = '/servers'  # API 기본 경로
    allow_list = True     # 목록 조회(GET) 허용
    ...
  • OpenStack의 자원(서버, 이미지 등)이 어떻게 구성되었는지 정의하는 '설계도'
  • API 명세와 직접 매핑되며, Proxy 계층이 작업을 처리할 때 참조하는 데이터 모델 역할을 함

사용자conn.compute.servers() 호출Connection (연결)Proxy (작업 지시)Resource (설계도 참조)OpenStack API
OpenStack SDK는 위와 같은 명확한 흐름으로 동작하며 이처럼 잘 정의된 계층 구조 덕분에 개발자는 복잡한 API 통신의 내부 과정을 신경 쓸 필요 없이, 필요한 결과물을 얻을 수 있다.

2주차 과제

필수과제

openstack server list의 경우 위와 같이 한정된 값만을 출력해주는데 해당 결과값에 project name , user name을 포함시켜서 출력

1.프로젝트 및 사용자 정보 캐싱

# /openstackclient/compute/v2/server.py
class ListServer(command.Lister):
    _description = _("List servers")
    ...
    def take_action(self, parsed_args):
    	...
        try:
            projects_map = {p.id: p.name for p in identity_client.projects.list()}
    		users_map = {u.id: u.name for u in identity_client.users.list()}
        except Exception as e:
            LOG.warning("Could not pre-fetch projects or users: %s", e)
            projects_map = {}
            users_map = {}

        search_opts = {
            'reservation_id': parsed_args.reservation_id,
            ...

Project와 User 이름을 미리 조회하여 맵으로 만듭니다.

이때 openstacksdk에서는 리소스 목록을 가져올 때 .list()를 명시적으로 호출해야 합니다.

따라서 리소스 목록을 가져올 때 identity_client.projects.list()와 같이 사용해야 합니다.

2. 출력 columns, column_headers 추가

# /openstackclient/compute/v2/server.py
class ListServer(command.Lister):
    _description = _("List servers")
    ...
    def take_action(self, parsed_args):
    	...
        columns: tuple[str, ...] = (
            'id',
            'name',
            'status',
        )
        column_headers: tuple[str, ...] = (
            'ID',
            'Name',
            'Status',
        )

        columns += ('project_name', 'user_name')
        column_headers += ('Project Name', 'User Name')
        ...

columnscolumn_headers 튜플에 Project NameUser Name을 추가합니다.

3. 각 서버 객체에 이름 정보 추가

# /openstackclient/compute/v2/server.py
class ListServer(command.Lister):
    _description = _("List servers")
    ...
    def take_action(self, parsed_args):
    	...
        data = list(compute_client.servers(**search_opts))

        for s in data:
            s.project_name = projects_map.get(s.project_id, s.project_id)
            s.user_name = users_map.get(s.user_id, s.user_id)
        
        images = {}
        flavors = {}
        ...

compute_client.servers(**search_opts)를 통해 서버 목록인 data를 가져온 후, 이 목록을 순회하며 각 서버 객체인 sproject_nameuser_name 속성을 동적으로 추가합니다.

4. openstack server list 확인

// launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python 디버거: 인수가 있는 현재 파일",
            "type": "debugpy",
            "request": "launch",
            "program": "python-openstackclient/openstackclient/shell.py",
            "console": "integratedTerminal",
            "args": [
                "server",
                "list"
            ],
            "env": {
                "OS_PROJECT_NAME": "admin",
                "OS_TENANT_NAME": "admin",
                "OS_USERNAME": "admin",
                "OS_PASSWORD": "<PW>",
                "OS_REGION_NAME": "RegionOne",
                "OS_IDENTITY_API_VERSION": "3",
                "OS_AUTH_TYPE": "password",
                "OS_AUTH_URL": "http://<ENDPOINT>/identity",
                "OS_USER_DOMAIN_ID": "default",
                "OS_PROJECT_DOMAIN_ID": "default",
                "OS_VOLUME_API_VERSION": "3"
            }
        }
    ]
}

위와 같이 launch.json"program": "python-openstackclient/openstackclient/shell.py"args를 설정하고 실행시키면

$ OCA-OpenStack\\Scripts\\python.exe c:\\Users\\lenovo\\.cursor\\extensions\\ms-python.debugpy-2024.6.0-win32-x64\\bundled\\libs\\debugpy\\adapter/../..\\debugpy\\launcher 51322 -- python-openstackclient/openstackclient/shell.py server list 
+--------------------------------------+-----------------+--------+--------------+-----------+----------------------------------------+--------------------------+-----------+
| ID                                   | Name            | Status | Project Name | User Name | Networks                               | Image                    | Flavor    |
+--------------------------------------+-----------------+--------+--------------+-----------+----------------------------------------+--------------------------+-----------+
| 182eef16-2d78-41c0-a962-c2e75cfc0699 | cirros-instance | ACTIVE | admin        | admin     | shared=192.168.100.59, 192.168.233.187 | N/A (booted from volume) | cirros256 |
+--------------------------------------+-----------------+--------+--------------+-----------+----------------------------------------+--------------------------+-----------+

위와 같이 Project NameUser Name이 함께 출력되는 것을 확인할 수 있습니다.

선택 과제

openstack server list를 입력했을 때 take_action() 함수를 찾아가는 과정 분석해보기

# shell.py
def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    return OpenStackShell().run(argv)


if __name__ == "__main__":
    sys.exit(main())

args: ["server", "list"]shell.py 스크립트에 Command Line 인자로 전달되며 코드 내에서는 sys.argv를 통해 이 값을 받게 됩니다.

이때 main() 메서드의 역할은 ['server', 'list']를 받아 OpenStackShell 클래스의 run() 메서드에 그대로 넘겨주는 것입니다.

# shell.py
class OpenStackShell(shell.OpenStackShell):
	...
    def initialize_app(self, argv):
        super().initialize_app(argv)

        # Re-create the client_manager with our subclass
        self.client_manager = clientmanager.ClientManager(
            cli_options=self.cloud,
            api_version=self.api_version,
            pw_func=shell.prompt_for_password,
        )

setup.cfg에 정의된 진입점으로, OpenStackShell 앱을 생성하고 실행합니다.

# osc-lib/osc_lib/shell.py
class OpenStackShell(app.App):
	...
	def run(self, argv: list[str]) -> int:
        ret_val = 1
        self.command_options = argv
        try:
            ret_val = super().run(argv)
            return ret_val
    ...

openstackclientOpenStackShell은 위 클래스를 상속받아 실제 run() 로직의 대부분은 이 부모 클래스와, 이 클래스가 다시 호출하는 cliff.app.App에 의해 처리됩니다.

# Lib/site-packages/cliff/commandmanager.py
class CommanManager:
	...
    def find_command(
        self, argv: list[str]
        ...
            if found:
                cmd_ep = self.commands[found]
                if hasattr(cmd_ep, 'resolve'):
                    cmd_factory = cmd_ep.resolve()
                else:
                    # NOTE(dhellmann): Some fake classes don't take
                    # require as an argument. Yay?
                    arg_spec = inspect.getfullargspec(cmd_ep.load)
                    if 'require' in arg_spec[0]:
                        cmd_factory = cmd_ep.load(require=False)
                    else:
                        cmd_factory = cmd_ep.load()
                return (cmd_factory, return_name, search_args)
        ...

Cliff는 Python으로 커맨드라인 프로그램(CLI)을 개발하기 위한 프레임워크입니다. OpenStackClient와 같이 복잡한 다단계 명령어를 가진 애플리케이션을 만드는 데 주로 사용됩니다.

run_subcommand 메서드 내부에서 cliff.commandmanager.CommandManager의 인스턴스인 self.command_manager를 사용하여 find_command를 호출하고, 인자로 넘어온 ['server', 'list']에 해당하는 ListServer 클래스를 찾아냅니다.

run_subcommand 메서드가 find_command를 통해 찾아낸 cmd_factoryListServer 클래스를 사용하여 cmd = cmd_factory()를 통해 인스턴스를 생성합니다.

# Lib/site-packages/cliff/command.py
class Command(metaclass=abc.ABCMeta):
    def run(self, parsed_args: argparse.Namespace) -> int:
        """Invoked by the application when the command is run.

        Developers implementing commands should override
        :meth:`take_action`.

        Developers creating new command base classes (such as
        :class:`Lister` and :class:`ShowOne`) should override this
        method to wrap :meth:`take_action`.

        Return the value returned by :meth:`take_action` or 0.
        """
        parsed_args = self._run_before_hooks(parsed_args)
        return_code = self.take_action(parsed_args) or 0
        return_code = self._run_after_hooks(parsed_args, return_code)
        return return_code

ListServer를 포함한 모든 명령어 클래스의 부모 클래스인 /cliff/command.pyrun() 메서드가 self.take_action(parsed_args)를 호출함으로써, 실제 로직이 담긴 메서드로 연결됩니다.

# python-openstackclient/openstackclient/compute/v2/server.py
class ListServer(command.Lister):
    _description = _("List servers")
    ...
     def take_action(self, parsed_args):
     ...

따라서 최종적으로 python-openstackclient/openstackclient/compute/v2/server.pyListServer 클래스의 take_action 메서드가 호출됩니다.


3주차

  • 코드 리뷰를 받게 되면 내 코드를 직접 검증해야한다
  • 코드의 변경사항은 사소한 부분이라도 테스트 코드를 작성해야 한다
  • 통합 (end-to-end) -> 기능 (functional) -> 단위 테스트 (Unit)

  • OpenStack SDK/CLI는 주로 BDD 구조를 따른다
    • Behavior Driven Development
    • 시스템의 동작/행위 기반으로 한 테스트 코드
      • Given: 테스트 대상 행위를 일으키기 위한 초기 셋업
      • When: 테스트 대상 행위를 발생시키는 이벤트 기술
      • Then 이벤트 발생 시 기대하는 결과값들을 확인

테스트 더블

  • 테스트를 목적으로 실제 객체, 연관된 객체를 직접 사용하기 어려울 때 대신 사용하는 가짜 객체들을 의미
  • Stub, Mock 등 사용
  • 테스트를 하고 싶은 실행 흐름 내에서 가짜 객체가 필요한 경우가 존재한다
  • 테스트 내부에서 외부 의존성을 강제 주입해서 Unit Test를 할 수 있다
    • ex. 외부 API를 호출하는 객체
    • ex. 테스트 대상이 아닌 다른 클래스 등
    • ex. 특정 유틸성 함수 등
  • 파이썬 테스트 라이브러리 : unittest, pytest (pytest를 주로 쓴다)

OpenStack Keystone

  • 다른 서비스에 도달해도 해당 서비스는 또 다시 Keystone에 키 검증을 요청한다
  • Keystone은 중앙 집중형 인증을 위해 존재한다
  • 만약 Keystone이 존재하지 않으면 각각의 컴포넌트들이 매번 인증을 진행해야 한다
  • 따라서 코드가 더 복잡해질 수 것이다
  • K8s Keycloak과 같은 SSO와 유사한 개념이다

  • 위와 같이 Keystone HA 구성을 할 수 있다
  • 위와 같은 구성이 가장 기본적인 구성이다

Fernet Token

  • Keystone은 Fernet Token이라는 토큰 구조를 사용한다
  • MessagePacked라는 라이브러리를 사용한다
  • Key 저장소가 필요한데 해당 키는 토큰의 페이로드 구성 정보를 암호/복호화한다
  • 세 가지 상태 중 하나를 가진다
    • Primary: 실제 암호화/복호화 시 사용, 단 하나, 가장 높은 인덱스
    • Secondary: 보조키, 토큰을 해독만 할 수 있다
    • Staged: 준비키, 하나만, 반드시 있어야, 해독만할 수 있다, Primary Key가 된 적이 없고 저장소에서 0이란 이름을 가진다
  • Staged Key -> Promary Key -> Secondary Key

Rotate and Distribute Keys

Distribute Keys

  • Rsync: 원격으로 파일과 디렉터리를 복사하고 동기화하기 위해 사용하는 툴

Key Rotation

  • 현재 Staged 키를 Promary 키로 승격하고 새로운 Staged 키를 만들고 오래된 Secondary 키를 제거하는 단일 작업
  • Distribution과 분리된 작업이다
  • 단일 노드에서 이 작업을 수행하고 클러스터의 나머지 부분에 Key 저장소를 배포하기 전에 Rotation이 잘 이루어졌는지 확인해야 한다

Openstack Nova

nova daemon

  • nova-api: End User의 Compute API 호출을 허용하고 응답
  • nova-api-metadata: 인스턴스 메타데이터 요청 수락
  • nova-compute: 하이퍼바이저 API를 통해 VM 인스턴스를 생성하고 종료하는 데몬
    • 하이퍼바이저에는 XenServer/KVM/VMWare
  • nova-scheduler: Queue에서 VM 인ㄴ스턴스의 Request를 가져와 가상 시스템이 실행되는 Compute 서버 Node를 결정
  • nova-conductor: nova-compute 서비스와 데이터베이스 간에 상호작용 관리
  • nova-novncproxy: VNC 연결을 통해 실행 중인 인스턴스에 액세스하는 프록시 제공

Hypervisor

  • 하이퍼바이저는 가상 머신을 만들고 실행하는 컴퓨터 SW, 펌웨어 또는 하드웨어의 한 부분
  • Nova에서는 각 호스트가 하이퍼바이저를 실행한다
  • 관리자는 하이퍼바이저에 현재 실행 중인 모든 가상 서버와 같은 정보와 CPU, Mem 등 정보 쿼리 가능

인스턴스 생성까지의 처리 흐름

  • 모든 과정의 RabbitMQ에 의존하고 있기 때문에, RabbitMQ의 성능이 중요하다
  • RabbitMQ를 잘 운영해야 OpenStack에 문제가 없을 정도이다

3주차 과제

[필수 과제]

2주차에 진행한 server list 확장 코드에서 tox를 통해 unit test를 실행시키면 Test Failed가 발생합니다.
openstackclient.tests.unit.compute 경로에 있는 server list에 대한 unit test를 수정하여 tox 실행시 모든 unit test에서 성공하도록 해주세요

제출 내용 1: 수정된 unit test code (스크린샷 또는 실제 코드)
제출 내용 2: 아래의 예시와 같이 tox를 통해 모든 unit test 통과한 화면

[TroubleShooting] ImportError: Start directory is not importable

$ tox
...
py3: commands[0]> stestr run
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\.tox\py3\Lib\site-packages\stestr\subunit_runner\run.py", line 88, in <module>
    main()
  File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\.tox\py3\Lib\site-packages\stestr\subunit_runner\run.py", line 82, in main
    program.TestProgram(
  File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\.tox\py3\Lib\site-packages\stestr\subunit_runner\program.py", line 179, in __init__
    self.parseArgs(argv)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\main.py", line 130, in parseArgs
    self._do_discovery(argv[2:])
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\main.py", line 253, in _do_discovery
    self.createTests(from_discovery=True, Loader=Loader)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\main.py", line 160, in createTests
    self.test = loader.discover(self.start, self.pattern, self.top)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\loader.py", line 308, in discover
    raise ImportError('Start directory is not importable: %r' % start_dir)
ImportError: Start directory is not importable: '${OS_TEST_PATH:-./openstackclient/tests/unit}'

tox 명령어를 실행했을때 위와 같은 오류가 발생하였습니다.

이는 tox가 테스트를 실행할 때, 테스트 파일의 위치를 잘못 읽어오면서 발생한 문제로

# python-openstackclient/.stestr.conf
[DEFAULT]
# test_path=${OS_TEST_PATH:-./openstackclient/tests/unit}
test_path=./openstackclient/tests/unit
top_dir=./
group_regex=([^\.]+\.)+

.stestr.conf 파일은 stestr의 핵심 설정 파일이며 stestrOpenStack 프로젝트에서 널리 사용되는 Test Runner 입니다.

stestr 설정 파일은 셸이 아니므로, ${...} 같은 변수 확장 문법을 해석하지 못합니다.

따라서 test_path의 값을 openstackclient/tests/unit이라는 실제 경로가 아닌, ${OS_TEST_PATH:-./openstackclient/tests/unit} 이라는 문자열 그대로 인식하여 존재하지 않는 디렉토리를 찾으려고 시도하기 때문에 계속해서 ImportError가 발생하였다고 판단하였습니다.

따라서 .stestr.conf 파일에서 변수 확장 문법을 제거하고 실제 경로만 남겨 문제를 해결하였습니다.

$ tox
...
openstackclient.tests.unit.compute.v2.test_server.TestServerListV273.test_server_list_with_unlocked_v273
--------------------------------------------------------------------------------------------------------

Captured traceback:
~~~~~~~~~~~~~~~~~~~
    Traceback (most recent call last):

      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\tests\unit\compute\v2\test_server.py", line 5440, in test_server_list_with_unlocked_v273
    self.assertCountEqual(self.columns, columns)

      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\case.py", line 1216, in assertCountEqual
    self.fail(msg)

      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\unittest\case.py", line 715, in fail
    raise self.failureException(msg)

    AssertionError: Element counts were not equal:
First has 0, Second has 1:  'Project Name'
First has 0, Second has 1:  'User Name'


Captured pythonlogging:
~~~~~~~~~~~~~~~~~~~~~~~
    Could not pre-fetch projects or users: 'Mock' object is not iterable

또한 이후 정상적으로 tox를 실행시켰을때 강의에서 안내주신 내용처럼 위와 같은 테스트 실패가 발생하였습니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes

이후 identity_fakes를 통해 ProjectUser에 대한 Mock 데이터를 임의로 만들어주기 위해 위와 같이 패키지를 import하였습니다.

class _TestServerList(TestServer):
    # Columns to be listed up.
    columns = (
        'ID',
        'Name',
        'Status',
        # columns 튜플에 Project Name과 User Name 추가
        'Project Name',
        'User Name',
        'Networks',
        'Image',
        'Flavor',
    )
    columns_long = (
        'ID',
        'Name',
        'Status',
        # columns_long 튜플에 Project Name과 User Name 추가
        'Project Name',
        'User Name',
        'Flavor ID',
        'Task State',
        'Power State',
        'Networks',
        'Image Name',
        'Image ID',
        'Flavor Name',
        'Flavor ID',
        'Availability Zone',
        'Pinned Availability Zone',
        'Host',
        'Properties',
        'Scheduler Hints',
    )
    columns_all_projects = (
        'ID',
        'Name',
        'Status',
        # columns_all_projects 튜플에 Project Name과 User Name 추가
        'Project Name',
        'User Name',
        'Networks',
        'Image',
        'Flavor',
        'Project ID',
    )
    
    def setUp(self):
        super().setUp()

        # Default params of the core function of the command in the case of no
        # commandline option specified.
        self.kwargs = {
            'reservation_id': None,
            'ip': None,
            'ip6': None,
            'name': None,
            'status': None,
            'flavor': None,
            'image': None,
            'compute_host': None,
            'project_id': None,
            'all_projects': False,
            'user_id': None,
            'deleted': False,
            'changes-since': None,
            'changes-before': None,
        }

        # 테스트를 위한 project, user id 설정
        self.project_id = 'project-id-for-testing'
        self.user_id = 'user-id-for-testing'

        # The fake servers' attributes. Use the original attributes names in
        # nova, not the ones printed by "server list" command.
        self.attrs = {
            # Mock 서버 생성 시의 속성 명시적으로 추가
            'project_id': self.project_id,
            'user_id': self.user_id,
            'status': 'ACTIVE',
            'OS-EXT-STS:task_state': 'None',
            'OS-EXT-STS:power_state': 0x01,  # Running
            'networks': {'public': ['10.20.30.40', '2001:db8::5']},
            'OS-EXT-AZ:availability_zone': 'availability-zone-xxx',
            'OS-EXT-SRV-ATTR:host': 'host-name-xxx',
            'Metadata': format_columns.DictColumn({}),
        }

        self.image = image_fakes.create_one_image()

        self.image_client.find_image.return_value = self.image
        self.image_client.get_image.return_value = self.image

        self.flavor = compute_fakes.create_one_flavor()
        self.compute_client.find_flavor.return_value = self.flavor
        self.attrs['flavor'] = {'original_name': self.flavor.name}

        # The servers to be listed.
        self.servers = self.setup_sdk_servers_mock(3)
        self.compute_client.servers.return_value = self.servers

        # 정의한 ID와 동일한 ID를 가진 Fake 프로젝트/사용자 객체를 생성
        self.project = identity_fakes.FakeProject.create_one_project(
            attrs={'id': self.project_id}
        )
        self.user = identity_fakes.FakeUser.create_one_user(
            attrs={'id': self.user_id}
        )

이후 위와 같이 테스트의 기본 설정을 담당하는 _TestServerList 클래스에서 예상 컬럼 목록을 수정합니다.

또한 임의로 정의한 ID와 동일한 ID를 가진 Fake 프로젝트/사용자 객체를 생성하도록 setUp()을 수정합니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class TestServerList(_TestServerList):
       def test_server_list_long_option(self):
        self.data = tuple(
            (
                s.id,
                s.name,
                s.status,
                # project name과 user name 컬럼 추가
                # 이때 추가하는 순서에 주의해야 함
                self.project.name,
                self.user.name,
                getattr(s, 'task_state'),
                server.PowerStateColumn(getattr(s, 'power_state')),
                server.AddressesColumn(s.addresses),
                # Image will be an empty string if boot-from-volume
                self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                self.flavor.name,
                s.flavor['id'],
                ...

또한 실제로 테스트를 수행하는 test_server_list_long_option() 등의 메소드에 대해 self.data 튜플에 컬럼들을 모두 추가해줍니다.

이때, 추가하는 순서에 주의해야 합니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class TestServerListV273(_TestServerList):
    # Columns to be listed up.
    columns = (
        'ID',
        'Name',
        'Status',
        'Project Name',
        'User Name',
        'Networks',
        'Image',
        'Flavor',
    )
    columns_long = (
        'ID',
        'Name',
        'Status',
        'Project Name',
        'User Name',
        'Task State',
        'Power State',
        'Networks',
        'Image Name',
        'Image ID',
        'Flavor',
        'Availability Zone',
        'Pinned Availability Zone',
        'Host',
        'Properties',
        'Scheduler Hints',
    )

    def setUp(self):
        super().setUp()

        # The fake servers' attributes. Use the original attributes names in
        # nova, not the ones printed by "server list" command.
        self.attrs['flavor'] = {
            'vcpus': self.flavor.vcpus,
            'ram': self.flavor.ram,
            'disk': self.flavor.disk,
            'ephemeral': self.flavor.ephemeral,
            'swap': self.flavor.swap,
            'original_name': self.flavor.name,
            'extra_specs': self.flavor.extra_specs,
        }

        # The servers to be listed.
        self.servers = self.setup_sdk_servers_mock(3)
        self.compute_client.servers.return_value = self.servers

        Image = collections.namedtuple('Image', 'id name')
        self.image_client.images.return_value = [
            Image(id=s.image['id'], name=self.image.name)
            # Image will be an empty string if boot-from-volume
            for s in self.servers
            if s.image
        ]

        # The flavor information is embedded, so now reason for this to be
        # called
        self.compute_client.flavors = mock.NonCallableMock()

        self.data = tuple(
            (
                s.id,
                s.name,
                s.status,
                # 순서에 맞게 project name과 user name 추가
                self.project.name,
                self.user.name,
                server.AddressesColumn(s.addresses),
                # Image will be an empty string if boot-from-volume
                self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                self.flavor.name,
            )
            for s in self.servers
        )

TestServerListV273 클래스에 대해서도 동일하게 columns의 정보를 추가합니다.

해당 클래스의 경우 이미 _TestServerList를 상속받고 있기 때문에 새로운 ID와 Mock Identity를 생성하지는 않고, self.data만 수정합니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class TestServerListV273(_TestServerList):
    def test_server_list_v269_with_partial_constructs(self):
        self.set_compute_api_version('2.69')
        arglist = []
        verifylist = []
        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
        # include "partial results" from non-responsive part of
        # infrastructure.
        server_dict = {
            "id": "server-id-95a56bfc4xxxxxx28d7e418bfd97813a",
            "status": "UNKNOWN",
            "tenant_id": "6f70656e737461636b20342065766572",
            "created": "2018-12-03T21:06:18Z",
            "links": [
                {"href": "http://fake/v2.1/", "rel": "self"},
                {"href": "http://fake", "rel": "bookmark"},
            ],
            "networks": {},
        }
        fake_server = _server.Server(**server_dict)
        self.servers.append(fake_server)
        columns, data = self.cmd.take_action(parsed_args)
        # get the first three servers out since our interest is in the partial
        # server.
        next(data)
        next(data)
        next(data)
        partial_server = next(data)
        expected_row = (
            'server-id-95a56bfc4xxxxxx28d7e418bfd97813a',
            None,
            'UNKNOWN',
            # self.project.name 대신, 위 server_dict에 하드코딩된 값 사용
            '6f70656e737461636b20342065766572',
            # self.user.name 대신, user_id가 없으므로 None 기대
            None,
            server.AddressesColumn(None),
            '',
            '',
        )
        self.assertEqual(expected_row, partial_server)

test_server_list_v269_with_partial_constructs() 메소드의 경우 하드코딩된 테스트 데이터를 사용하기 때문에 이에 맞게 tox의 출력결과를 기반으로 expected_row를 수정해줍니다.

==============================

Failed 1 tests - output below:

==============================



openstackclient.tests.unit.compute.v2.test_server.TestServerCreate.test_server_create_with_block_device_from_file

-----------------------------------------------------------------------------------------------------------------



Captured traceback:

~~~~~~~~~~~~~~~~~~~

    Traceback (most recent call last):



      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\tests\unit\compute\v2\test_server.py", line 2736, in test_server_create_with_block_device_from_file

    parsed_args = self.check_parser(self.cmd, arglist, verifylist)

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\tests\unit\utils.py", line 86, in check_parser

    parsed_args = cmd_parser.parse_args(args)

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 1904, in parse_args

    args, argv = self.parse_known_args(args, namespace)

                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 1914, in parse_known_args

    return self._parse_known_args2(args, namespace, intermixed=False)

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 1943, in _parse_known_args2

    namespace, args = self._parse_known_args(args, namespace, intermixed)

                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 2184, in _parse_known_args

    start_index = consume_optional(start_index)

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 2113, in consume_optional

    take_action(action, args, option_string)



      File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.2800.0_x64__qbz5n2kfra8p0\Lib\argparse.py", line 2018, in take_action

    action(self, namespace, argument_values, option_string)



      File "C:\Users\lenovo\Git\2025-OSCA\OCA-OpenStack\python-openstackclient\openstackclient\compute\v2\server.py", line 1068, in __call__

    with open(values) as fh:

         ^^^^^^^^^^^^



    PermissionError: [Errno 13] Permission denied: 'C:\\Users\\lenovo\\AppData\\Local\\Temp\\tmpgr_188ci'

또한 현재 저는 Window 환경에서 테스트를 수행하고 있기 때문에 위와 같이 temp 파일을 다루는 테스트 로직 상에서 PermissionError가 발생하였습니다.

# python-openstackclient/openstackclient/tests/unit/compute/v2/test_server.py
class 
import os
...
TestServerCreate(TestServer):
...
    def test_server_create_with_block_device_from_file(self):
        self.set_compute_api_version('2.67')

        block_device = {
            'uuid': self.volume.id,
            'source_type': 'volume',
            'destination_type': 'volume',
            'disk_bus': 'ide',
            'device_type': 'disk',
            'device_name': 'sdb',
            'guest_format': 'ext4',
            'volume_size': 64,
            'volume_type': 'foo',
            'boot_index': 1,
            'delete_on_termination': True,
            'tag': 'foo',
        }

        # with 문을 제거하고 파일을 직접 열기
        fp = tempfile.NamedTemporaryFile(mode='w+', delete=False)
        # 테스트 종료 후 파일을 자동으로 삭제하도록 예약
        self.addCleanup(os.remove, fp.name)

        json.dump(block_device, fp=fp)
        fp.flush()

        # 파일 핸들을 명시적으로 닫아줌
        fp.close()

        # with tempfile.NamedTemporaryFile(mode='w+') as fp:
        #     json.dump(block_device, fp=fp)
        #     fp.flush()

        arglist = [
            '--image',
            self.image.id,
            '--flavor',
            self.flavor.id,
            '--block-device',
            fp.name,
            self.server.name,
        ]
        verifylist = [
            ('image', self.image.id),
            ('flavor', self.flavor.id),
            ('block_devices', [block_device]),
            ('server_name', self.server.name),
        ]
        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
        columns, data = self.cmd.take_action(parsed_args)

이는 Windows 환경에서 tempfile로 생성된 임시 파일에 접근할 때 발생하는 전형적인 권한 문제로 test_server_create_with_block_device_from_file() 메소드에서 with 문을 제거하고 직접 파일을 생성하고 테스트 종료 후 자동으로 삭제하도록 예약합니다.

최종적으로 위와 같이 새롭게 만들어진 Fake Identity에 대해서 모든 테스트가 정상적으로 수행됨을 볼 수 있었습니다.

멘토님께서 말씀주신 내용에 따라 새로운 Fake Identity가 아니라, 기존에 생성된 Fake Identity를 사용하는 방식으로 변경을 시도해볼 예정입니다.


4주차

OpenStack Barbican

  • K8s의 Secret과 유사한 형태로 Secret Key들을 관리
  • Barbican의 기능
    • Secret : 사용자 또는 서비스가 등록한 민감 정보를 저장
    • Container : 여러 Secret을 하나의 그룹으로 묶어서 관리
    • Order : 사용자가 직접 payload를 넣지 않고 대상 암호화만 하고 싶을때 사용 (Cinder 등)
    • ACL : 특정 사용자에게 secret에 대한 권한을 부여하거나 제한
  • Barbican의 경우 Default로 대칭키 (AES)로 암호화됨
  • 그 외에도 TLS 저장시 RSA, HMAC, KDF 등의 알고리즘이 존재
  • Barbican ACL에 허용한 경우만 접근이 가능하다
  • 실무에서는 LB를 만들때 사용한다

    https://github.com/kubernetes/cloud-provider-openstack

vs K8s Secret

  • K8s의 경우 Cluster 내부에서 사용되고 etcd에 저장됨
  • base64로 인코딩 (별도 암호화X)
  • 실제 암호화를 위해서는 Encryption을 사용하거나 외부 Payload를 사용

Barbican 실습

export OS_AUTH_URL=http://<Barbican 설치 DevStack>/identity
export OS_USERNAME="_"
export OS_PASSWORD="_"
export OS_PROJECT_DOMAIN_NAME="default"
export OS_PROJECT_NAME="admin"
export OS_USER_DOMAIN_NAME="default"
export OS_REGION_NAME="RegionOne"
export OS_INTERFACE="public"
export OS_IDENTITY_API_VERSION=3
export OS_AUTH_TYPE="password"

기존에 설정한 DevStack에서 임시로 환경변수를 위와 같이 설정하여 Barbican이 설치된 DevStack에 접근할 수 있었습니다.

ubuntu@msy-devstack:~$ openstack secret list
+-------------+----------+----------+--------+---------------+-----------+------------+-------------+------+------------+
| Secret href | Name     | Created  | Status | Content types | Algorithm | Bit length | Secret type | Mode | Expiration |
+-------------+----------+----------+--------+---------------+-----------+------------+-------------+------+------------+
| http://---. | my-      | 2025-08- | ACTIVE | {'default':   | aes       |        256 | opaque      | cbc  | None       |
| ----------- | secret2  | 05T05:49 |        | 'text/plain'} |           |            |             |      |            |
| /key-manage |          | :34+00:0 |        |               |           |            |             |      |            |
| r/v1/secret |          | 0        |        |               |           |            |             |      |            |
| s/3f7a9299- |          |          |        |               |           |            |             |      |            |
| 0c3c-45c3-  |          |          |        |               |           |            |             |      |            |
| 941a-       |          |          |        |               |           |            |             |      |            |
| de088d76a38 |          |          |        |               |           |            |             |      |            |
| c           |          |          |        |               |           |            |             |      |            |
| http://---. | my-      | 2025-08- | ACTIVE | {'default':   | aes       |        256 | opaque      | cbc  | None       |
| ----------- | secret   | 05T05:14 |        | 'text/plain'} |           |            |             |      |            |
| /key-manage |          | :39+00:0 |        |               |           |            |             |      |            |
| r/v1/secret |          | 0        |        |               |           |            |             |      |            |
| s/90576150- |          |          |        |               |           |            |             |      |            |
| df00-45b0-  |          |          |        |               |           |            |             |      |            |
| bf05-       |          |          |        |               |           |            |             |      |            |
| 0b0e25b0994 |          |          |        |               |           |            |             |      |            |
| 3           |          |          |        |               |           |            |             |      |            |
+-------------+----------+----------+--------+---------------+-----------+------------+-------------+------+------------+

또한 위와 같이 성공적으로 Secret을 받아오는 것을 볼 수 있었다.

1. Barbican secret 생성

$ openstack secret store \
--name "msy-secret" \
--payload "msy-secret-password" \
--payload-content-type text/plain

+---------------+---------------------------------------------------------------------+
| Field         | Value                                                               |
+---------------+---------------------------------------------------------------------+
| Secret href   | http://_/key-                                         |
|               | manager/v1/secrets/5a985775-46da-4829-865e-4954ca95c68c             |
| Name          | msy-secret                                                          |
| Created       | None                                                                |
| Status        | None                                                                |
| Content types | {'default': 'text/plain'}                                           |
| Algorithm     | aes                                                                 |
| Bit length    | 256                                                                 |
| Secret type   | opaque                                                              |
| Mode          | cbc                                                                 |
| Expiration    | None                                                                |
+---------------+---------------------------------------------------------------------+

2. 생성한 secret에 저장한 값 확인해보기

$ openstack secret get 5a985775-46da-4829-865e-4954ca95c68c --payload
+---------+---------------------+
| Field   | Value               |
+---------+---------------------+
| Payload | msy-secret-password |
+---------+---------------------+

$ openstack secret get 5a985775-46da-4829-865e-4954ca95c68c --decrypt
+---------+---------------------+
| Field   | Value               |
+---------+---------------------+
| Payload | msy-secret-password |
+---------+---------------------+

$ openstack secret get 5a985775-46da-4829-865e-4954ca95c68c --file msy-secret.txt
$ cat msy-secret.txt
msy-secret-password

3. Barbican container 생성

# secret을 하나 더 생성
$ openstack secret store \
--name "msy-secret-2" \
--payload "msy-secret-password2" \
--payload-content-type text/plain

# 두개의 secret을 통해 container 생성
op secret container create --name "msy-container-name" \
--type generic \
--secret "msy_key_1={$secret-url-1}" \
--secret "msy_key-2={$secret-url-2}"

+----------------+--------------------------------------------------------------------+
| Field          | Value                                                              |
+----------------+--------------------------------------------------------------------+
| Container href | http://---------------/key-                                        |
|                | manager/v1/containers/fc0ab38d-d8c8-4bbc-bbdc-75aa6b5d1013         |
| Name           | msy-container-name                                                 |
| Created        | None                                                               |
| Status         | ACTIVE                                                             |
| Type           | generic                                                            |
| Secrets        | msy_key_1=http://---------------/key-                              |
|                | manager/v1/secrets/5a985775-46da-4829-865e-4954ca95c68c            |
|                | msy_key-2=http://---------------/key-                              |
|                | manager/v1/secrets/80abea49-581a-4729-95c7-6f5f17ed41aa            |
| Consumers      | None                                                               |
+----------------+--------------------------------------------------------------------+

4. Barbican container를 --type certificate으로 생성

openstack secret container create --name "msy-container-name-certificate" \
--type certificate \
--secret "msy_key_1={$secret-url-1}" \
--secret "msy_key-2={$secret-url-2}"

+----------------+-----------------------------------------------------------------------------------+
| Field          | Value                                                                             |
+----------------+-----------------------------------------------------------------------------------+
| Container href | http://_______________/key-                                                       |
|                | manager/v1/containers/b30f942d-d9cf-4d0d-8d9c-3700fbee238b                        |
| Name           | msy-container-name-certificate                                                    |
| Created        | None                                                                              |
| Status         | ACTIVE                                                                            |
| Type           | certificate                                                                       |
| Certificate    | None                                                                              |
| Intermediates  | None                                                                              |
| Private Key    | None                                                                              |
| PK Passphrase  | None                                                                              |
| Consumers      | None                                                                              |
+----------------+-----------------------------------------------------------------------------------+

Barbican은 --type certificate 컨테이너를 생성할 때, 어떤 시크릿이 '인증서'이고 어떤 것이 '개인 키'인지 구분하기 위해 정해진 키 이름을 찾습니다.

따라서 msy_key_1이 무엇을 의미하는지 알 수 없으므로, 이를 무시하고 Certificate, Private Key 등의 주요 필드를 None으로 남겨둔 채 비어있는 컨테이너 셸만 생성한 것입니다.

openstack secret container create \
  --name "msy-container-certificate-tls" \
  --type certificate \
  --secret "certificate=http://---------------/key-manager/v1/secrets/5a985775-46da-4829-865e-4954ca95c68c" \
  --secret "private_key=http://---------------/key-manager/v1/secrets/80abea49-581a-4729-95c7-6f5f17ed41aa"
  
+----------------+-----------------------------------------------------------------------------------+
| Field          | Value                                                                             |
+----------------+-----------------------------------------------------------------------------------+
| Container href | http://---------------/key-                                                       |
|                | manager/v1/containers/b8d8e5ae-e233-4d28-961c-9b3eac6c7f63                        |
| Name           | msy-container-certificate-tls                                                     |
| Created        | None                                                                              |
| Status         | ACTIVE                                                                            |
| Type           | certificate                                                                       |
| Certificate    | http://---------------/key-                                                       |
|                | manager/v1/secrets/5a985775-46da-4829-865e-4954ca95c68c                           |
| Intermediates  | None                                                                              |
| Private Key    | http://---------------/key-                                                       |
|                | manager/v1/secrets/80abea49-581a-4729-95c7-6f5f17ed41aa                           |
| PK Passphrase  | None                                                                              |
| Consumers      | None                                                                              |
+----------------+-----------------------------------------------------------------------------------+

따라서 위와 같이 올바른 키 이름으로 명령어를 실행하면, 출력 결과의 CertificatePrivate Key 필드가 정상적으로 채워지는 것을 확인할 수 있었습니다.

Functional Test

  • 실제 DevStack에서 코드를 돌려서 잘 실행이 되는지를 검사하는 Test
  • 지금까지는 Unit Test가 필수였음
  • 하지만 이전까지 Functional Test가 없어도 통과시킴
  • 따라서 현재 Functional Test가 없는 경우가 정말 많음
  • 따라서 Functional Test를 만드는 것이 다음 과제가 될 것

이번 4주차 강의 도중 [Keystone]의 openstack token issue를 선택하여 이에 대한 Functional Test를 구현하게 될 예정입니다.

Gerrit vs Github

  • Github 이전에 opendev를 사용
  • 최종적으로는 위 과정을 모두 거친 이후 Github에 미러링이 되는 형식

https://docs.openstack.org/contributors/ko_KR/common/git.html

  • 위와 같이 별도의 Commit Rule이 존재
<Short Summary Line (<= 50 chars)>

<Optional Detailed Explanation (wrap at 72 chars)>

Closes-Bug: #123456
Change-Id: Iabc123... (auto-generated by Gerrit)
  1. “Tile”과 “Body” 를 나누기
  2. Title: 최대 50 글자 이내 (summary only)
    Body: 한줄에 72 글자 이내; explain why not just what
  3. “git commit -s”를 통해 “Signed-off-by”를 마지막에 남겨야 함
    • You can add “Closes-Bug” from launchpad
    • “Change-Id” and “Signed-off-by” is auto generated

LaunchPad

https://launchpad.net/openstack

  • 각 프로젝트의 릴리즈, 버그리포트, 로드맵 등이 정리되는 아카이브

4주차 과제

[필수 과제] openstack token issue CLI 사용법 정리 하기

openstack toiken issue
새로운 인증 정보를 입력받아 토큰을 생성하는 것이 아니라, 현재 인증된 (로그인된) 세션의 토큰 정보를 다시 조회하여 보여주는 역할을 합니다.
즉, 이미 발급받은 토큰의 ID, 만료 시간, 스코프(프로젝트, 도메인 등)와 같은 상세 정보를 확인하는 데 사용됩니다.

# openstackclient/identity/v3/token.py
class IssueToken(command.ShowOne):
    _description = _("Issue new token")

    # scoped token is optional
    required_scope = False

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        return parser

    def take_action(self, parsed_args):
        auth_ref = self.app.client_manager.auth_ref
        if not auth_ref:
            raise exceptions.AuthorizationFailure(
                _("Only an authorized user may issue a new token.")
            )

        data = {}
        if auth_ref.auth_token:
            data['id'] = auth_ref.auth_token
        if auth_ref.expires:
            datetime_obj = auth_ref.expires
            expires_str = datetime_obj.strftime('%Y-%m-%dT%H:%M:%S%z')
            data['expires'] = expires_str
        if auth_ref.project_id:
            data['project_id'] = auth_ref.project_id
        if auth_ref.user_id:
            data['user_id'] = auth_ref.user_id
        if auth_ref.domain_id:
            data['domain_id'] = auth_ref.domain_id
        if auth_ref.system_scoped:
            # NOTE(lbragstad): This could change in the future when, or if,
            # keystone supports the ability to scope to a subset of the entire
            # deployment system. When that happens, this will have to relay
            # scope information and IDs like we do for projects and domains.
            data['system'] = 'all'
        return zip(*sorted(data.items()))

우선 실제로 openstack token issue 명령어 사용법을 분석하기 이전에 openstackclient 코드를 분석하였습니다.

  1. IssueToken 클래스
  • IssueToken 클래스는 command.ShowOne을 상속받습니다.
  • openstackclient 라이브러리에서 ShowOne은 단일 객체의 정보를 Key-Value 형태의 테이블로 출력하는 데 사용되는 기반 클래스입니다.
    $ openstack token issue
    +------------+------------------------------------------------------------------+
    | Field      | Value                                                            |
    +------------+------------------------------------------------------------------+
    | domain_id  | default                                                          |
    | expires    | 2025-08-08T09:04:05+0000                                         |
    | id         | gAAAAABm3a...                                                    |
    | project_id | 2f83de4f5f5c439999321e244b4c4e7f                                 |
    | user_id    | 8a9a6b8b3e4c4e5a8f6d7e8f9a0b1c2d                                 |
    +------------+------------------------------------------------------------------+
    이러한 상속 구조에 의해 openstack token issue의 결과가 위처럼 2열 테이블 형식으로 나타납니다.
  1. get_parser()
  • 해당 메소드는 super()를 호출하기만 하고 추가적인 인자를 정의하지 않습니다.
  • 이는 openstack token issue 명령어가 자체적인 옵션을 가지지 않고, OpenStack 클라이언트의 전역 인증 옵션(OS_PROJECT_ID 환경변수 등)에 의존한다는 것을 의미합니다.
  1. take_action()
    a. 인증 정보 로드:

    • self.app.client_manager.auth_ref를 통해 현재 클라이언트 세션의 인증 정보 객체인 auth_ref를 가져옵니다.
    • 이 객체에는 이미 인증을 완료하며 얻은 토큰, 사용자 ID, 프로젝트 ID 등의 모든 정보가 담겨 있습니다.
    • 만약 로그인되지 않았다면 즉, 객체가 존재하지 않는다면 AuthorizationFailure 예외를 발생시킵니다.

    b. 데이터 추출 및 구성: auth_ref 객체에서 필요한 정보들을 추출하여 data 딕셔너리에 저장합니다.

    c. 출력 데이터 반환:

    • return zip(*sorted(data.items()))
    • data.items(): (key, value) 튜플의 리스트를 만듭니다.
    • sorted(): 키(Field)를 기준으로 알파벳순으로 정렬합니다.
    • 이 두 튜플을 반환하면 ShowOne 기반 클래스가 이를 받아 테이블 형식으로 출력합니다.
ubuntu@msy-devstack:~$ openstack --version
openstack 7.5.0

ubuntu@msy-devstack:~$ openstack token issue --help
usage: openstack token issue [-h] [-f {json,shell,table,value,yaml}] [-c COLUMN] [--noindent]
                             [--prefix PREFIX] [--max-width <integer>] [--fit-width]
                             [--print-empty]

Issue new token

options:
  -h, --help            show this help message and exit

output formatters:
  output formatter options

  -f {json,shell,table,value,yaml}, --format {json,shell,table,value,yaml}
                        the output format, defaults to table
  -c COLUMN, --column COLUMN
                        specify the column(s) to include, can be repeated to show multiple columns

json formatter:
  --noindent            whether to disable indenting the JSON

shell formatter:
  a format a UNIX shell can parse (variable="value")

  --prefix PREFIX
                        add a prefix to all variable names

table formatter:
  --max-width <integer>
                        Maximum display width, <1 to disable. You can also use the CLIFF_MAX_TERM_WIDTH
                        environment variable, but the parameter takes precedence.
  --fit-width           Fit the table to the display width. Implied if --max-width greater than 0. Set the
                        environment variable CLIFF_FIT_WIDTH=1 to always enable
  --print-empty         Print empty table if there is no data to show.

이후 위와 같이 NHN의 DevStack이 설치된 인스턴스에서 openstack 7.5.0 버전의 openstack token issue 명령어의 사용법을 출력하였습니다.

openstack token issue [option]

일반 옵션

  • -h, --help: 도움말 메시지를 보여주고 종료합니다.

출력 형식 옵션

  • -f, --format {json|shell|table|value|yaml}: 결과의 출력 형식을 지정합니다. (기본값: table)
  • -c, --column <COLUMN>: 출력 결과에서 보고 싶은 특정 column만 지정합니다.

Table 형식 옵션 (기본)

  • --max-width <숫자>: 테이블의 최대 너비를 지정합니다.
  • --fit-width: 현재 터미널 창의 너비에 맞춰 테이블 크기를 자동으로 조절합니다.
  • --print-empty: 표시할 데이터가 없을 경우, 빈 테이블의 틀이라도 출력합니다.

JSON 형식 옵션

  • --noindent: JSON 출력 시 자동으로 적용되는 들여쓰기를 비활성화하여 한 줄로 출력합니다.

Shell 형식 옵션

  • --prefix <PREFIX>: 변수명="값" 형태로 출력할 때, 모든 변수명 앞에 지정한 접두사를 붙입니다.

명령어의 옵션과 각 파라미터는 위와 같이 정리할 수 있었습니다.

[필수 과제] openstack token issue CLI에 대한 functional test 코드 구현하기

# /openstackclient/openstackclient/tests/functional/identity/v3/test_token.py
from openstackclient.tests.functional.identity.v3 import common

class TokenTests(common.IdentityTests):
    def test_token_issue(self):
        raw_output = self.openstack('token issue')
        items = self.parse_show(raw_output)
        self.assert_show_fields(items, self.TOKEN_FIELDS)

기존 Functional Test 코드는 self.assert_show_fields(items, self.TOKEN_FIELDS)을 통해 openstack token issue 명령어를 실행하고 나온 기본 table 형식의 출력을 분석합니다.

즉, 해당 테스트는 기대하는 필드들이 모두 포함되어 있는지 정도의 기본적인 구조만 검증할 뿐 직접적으로 어떠한 값이 존재하는지는 확인하지 않습니다.

# /openstackclient/openstackclient/tests/functional/identity/v3/test_token.py
import json
from openstackclient.tests.functional.identity.v3 import common

# 순수한 테스트 메소드
class TokenTests(common.IdentityTests):
    # TC1: 기본 테이블 형식 출력 검증
    def test_token_issue_table_format(self):
        raw_output = self.openstack('token issue')
        self.assertIn('expires', raw_output)
        self.assertIn('project_id', raw_output)
        self.assertIn('user_id', raw_output)

    # TC2: JSON 형식 출력 검증
    def test_token_issue_json_format(self):
        raw_output = self.openstack('token issue --format json')
        data = json.loads(raw_output)
        self.assertIn('id', data)
        self.assertIn('project_id', data)
        self.assertIn('user_id', data)

    # TC3: 특정 컬럼만 값으로 출력하는지 검증
    def test_token_issue_single_column(self):
        # project_id 필드가 존재하는지만 확인
        raw_output = self.openstack(
            'token issue --format value --column project_id'
        )
        # 출력이 비어있지 않은지만 확인
        self.assertNotEqual('', raw_output.strip())

따라서 위와 같이 명령어의 출력 포맷이 올바르게 작동하는지 검증할 수 있도록 테스트 코드를 수정하였습니다.

  • openstack('command'): OpenStack CLI 명령어를 실행하고 그 결과를 문자열로 반환합니다.
  • assertIn(expected, actual): actual 문자열 안에 expected 문자열이 포함되어 있는지 확인합니다.
  • assertEqual(expected, actual): expected 값과 actual 값이 정확히 동일한지 확인합니다.

테스트를 구성하는 주요 메소드는 위와 같습니다.

# .stestr.conf
[DEFAULT]
# test_path=./openstackclient/tests/unit
test_path=./openstackclient/tests/functional
top_dir=./
group_regex=([^\.]+\.)+

이후 위와 같이 Functional Test를 진행해주기 위해 test_path를 수정해줍니다.

# tox.ini
[testenv:unit-tips]
commands =
    python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff
    python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth
    python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib
    python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk
    python -m pip freeze
    stestr run {posargs}

[testenv:functional{,-tips,-py38,-py39,-py310,-py311,-py312}]
description =
    Run functional tests.
setenv =
    OS_TEST_PATH=./openstackclient/tests/functional
    # 인코딩 문제 해결
    PYTHONUTF8=1
passenv =
    OS_*
commands =
    tips: python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff
    tips: python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth1
    tips: python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib
    tips: python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk
    tips: python -m pip freeze
    {[testenv]commands}

또한 위와 같이 PYTHONUTF8=1를 추가하여 테스트 환경에서의 인코딩 문제를 해결해주고

# python-openstackclient/clouds.yaml
clouds:
  devstack-admin:
    auth:
      auth_url: http://<MSY-DEVSTACK-PUB-IP>/identity
      username: "admin"
      password: "<DEVSTACK-PW>"
      project_domain_name: "default"
      project_name: "admin"
      user_domain_name: "default"
    auth_type: "password"
    region_name: "RegionOne"
    interface: "public"
    identity_api_version: 3

테스트를 진행할 때의 접속 정보를 clouds.yaml파일에 위와 같이 추가해주고

$ tox -e functional -- "openstackclient.tests.functional.identity.v3.test_token.TokenTests"

위 명령어를 통해 수정한 identity.v3.test_token.TokenTests 클래스에 대해서만 테스트를 수행해주면

$ tox -e functional -- "openstackclient.tests.functional.identity.v3.test_token.TokenTests"
...
functional: install_package> python -I -m pip install --force-reinstall --no-deps C:\...\OCA-OpenStack\python-openstackclient\.tox\.tmp\package\28\python_openstackclient-8.1.1.dev14-0.editable-py3-none-any.whl
functional: commands[0]> stestr run openstackclient.tests.functional.identity.v3.test_token.TokenTests
{0} openstackclient.tests.functional.identity.v3.test_token.TokenTests.test_token_issue_json_format [1.703573s] ... ok
{0} openstackclient.tests.functional.identity.v3.test_token.TokenTests.test_token_issue_single_column [1.720266s] ... ok
{0} openstackclient.tests.functional.identity.v3.test_token.TokenTests.test_token_issue_table_format [1.712172s] ... ok

======
Totals
======
Ran: 3 tests in 5.1360 sec.
 - Passed: 3
 - Skipped: 0
 - Expected Fail: 0
 - Unexpected Success: 0
 - Failed: 0
Sum of execute time for each test: 5.1360 sec.

==============
Worker Balance
==============
 - Worker 0 (3 tests) => 0:00:05.136011
.pkg: _exit> python C:\...\OCA-OpenStack\Lib\site-packages\pyproject_api\_backend.py True pbr.build
  functional: OK (29.80=setup[7.67]+cmd[22.12] seconds)
  congratulations :) (30.25 seconds)

위와 같이 수정한 Functional Test가 DevStack에서 정상적으로 수행되는 모습을 볼 수 있습니다.


0개의 댓글