필수과제
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')
...
columns
와 column_headers
튜플에 Project Name
과 User 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
를 가져온 후, 이 목록을 순회하며 각 서버 객체인 s
에 project_name
과 user_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 Name
괴 User 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
...
openstackclient
의 OpenStackShell
은 위 클래스를 상속받아 실제 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_factory
인 ListServer
클래스를 사용하여 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.py
의 run()
메서드가 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.py
의 ListServer
클래스의 take_action
메서드가 호출됩니다.