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 메서드가 호출됩니다.

4개의 좋아요