Master Challenge - openstack server list 명령어 동작 원리 파악하기

Master Challenge - openstack server list 명령어 동작 원리 파악하기

들어가며

“openstack server list” 명령어를 입력하면 코드상에서 어떻게 명령어의 인자 값을 구별하고 어떻게 처리를 하는지, 어떻게 예쁘게 테이블 형태로 출력하는지 동작하는지 정리해보겠다. 이 글에선 결론과 간략하게 도출과정을 소개하겠습니다. 상세한 도출 과정은 먼저 오픈스택 팀 블로그에 올려둔 상세 정리본에 작성했으니 참고하시면 되겠습니다. 또 미션 1, 2번의 경우 도출과정이 너무너무 길어 노션에 더욱 상세히 정리했으니 참고하시면 될 거 같습니다 :)

OpenStack 팀 블로그: https://openstack-kr-contribution-academy-2021.readthedocs.io/ko/latest/

인자로 입력받은 server list 를 어떻게 구별해내는가

결론

OpenStackShell object 는 주어진 인자(예: server list)를 처리하기 전에 각 API version 에 대한 혹은 cli, common, extension 등과 같은 group 이 추가가 되고 각 group 에 해당하는 command 들이 dict 형식(key: “server list”, value: serverlist에 대한 EntryPoint object)으로 OpenStackShell object 에 업로드 된다. 그 후 주어진 인자 값이 해당 객체 내 command dict 에 존재하는 지 확인한다.

도출 과정

1
2
3
4
def initialize_app(self, argv):
self._load_plugins()

self._load_commands()

self._load_plugins() command 수행하면 각 PLUGIN_MODULES 에 해당하는 command_group 과 command 들을 CommandManager obejct 에 사전 타입으로 업로드한다.

  • PLUGIN_MODULES 는 다음과 같다.

[openstack.cli.base]

  • compute = openstackclient.compute.client
  • identity = openstackclient.identity.client
  • image = openstackclient.image.client
  • network = openstackclient.network.client
  • object_store = openstackclient.object.client
  • volume = openstackclient.volume.client

업로드 되는 command 는 key 값에 명령어 name, value 에는 EntryPoint 객체가 할당된다.

  • key: “server list”
  • value: EntryPoint(name=’server_list’, value=’openstackclient.compute.v2.server:ListServer’, group=’openstack.compute.v2’)

self._load_commands() command 를 통해 group_list 에 “openstack.common”, “openstack.extension” 이 추가가 되었고 그에 해당하는 command 들이 추가되었다.

1
2
3
4
5
6
7
class App(object):
...
def run_subcommand(self, argv):
try:
subcommand = self.command_manager.find_command(argv)
except ValueError as err:
...

find_command(argv) : App 클래스 find_command 메소드를 통해 주어진 server list 인자를 현재 OpenStackShell 에 포함된 command 에 있는 지 확인(구별)한다. 만약 인자로 주어진 명령어가 object 내의 command 에 존재하지 않으면 비슷한 (match 되는) 명령어 리스트를 보여주거나 에러를 발생시킨다.

server list 라는 명령어를 처리하는 파일은 무엇인가?

결론

server list 명령어를 처리해주는 파일은 openstack/python - openstackclient/openstackclient/compute/v2/server.py 이다.

도출과정

1번 문제에서 command 가 CommandManager 에 사전 형태로 업로드 되는 것을 알 수 있었다. 만약 “server list” 라는 명령어가 업로드된 command 안에 키 값으로 있다면 value 값을 리턴해준다. 이 value 는 EntryPoint 인스턴스이다. 이 EntryPoint 인스턴스는 name, value, group 에 대한 정보를 가지고 있다.

“server list”의 경우

  • name: “server_list”
  • value: “openstackclient.compute.v2.server:ListServer’”
  • group: “openstack.compute.v2”

server list 에 해당하는 EntryPoint 인스턴스 value 값에 이 명령어를 처리할 해당 파일과 객체의 정보가 있는 것을 볼 수 있다.

openstackcli 는 어떻게 nova api 주소를 알아내나요?

도출과정

1
2
3
4
5
6
# site-packages/keystoneauth1/identity/v3/base.py
def get_auth_ref(self, session, **kwargs):
...
resp = session.post(token_url, json=body, headers=headers,
authenticated=False, log=False, **rkwargs)
...
1
2
3
4
# site-packages/keystoneauth1/session.py
def post(self, url, **kwargs):

return self.request(url, 'POST', **kwargs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# site-packages/keystoneauth1/session.py
def request(self, url, method, json=None, original_ip=None,
user_agent=None, redirect=None, authenticated=None,
endpoint_filter=None, auth=None, requests_auth=None,
raise_exc=True, allow_reauth=True, log=True,
endpoint_override=None, connect_retries=None, logger=None,
allow=None, client_name=None, client_version=None,
microversion=None, microversion_service_type=None,
status_code_retries=0, retriable_status_codes=None,
rate_semaphore=None, global_request_id=None,
connect_retry_delay=None, status_code_retry_delay=None,
**kwargs):

...
resp = send(**kwargs)
...
return resp

resp 는 kwargs(headers, auth 정보)를 가지고 ‘http://<오픈스택 구축 설정 ip 주소>/identity/v3/auth/tokens‘ 에서 다음과 같은 “catalog” 값으로 nova, keystone, cinder, glance 등 모든 컴포넌트들의 api 주소를 가져온다.

1
2
3
# resp 중 "catalog" nova 정보
"catalog":
[ ... {"endpoints": [{"id": "e7507720bc274e56b420466613be3f07", "interface": "public", "region_id": "RegionOne", "url": "http://211.37.148.128/compute/v2.1", "region": "RegionOne"}], "id": "3e7dec3e86ea4652ad633484b07fa368", "type": "compute", "name": "nova"}, ... ]

nova 의 어떤 API를 호출하여 결과를 받아오나요? ( 어떤 URI 를 호출하나요? )

결론

http://<오픈스택 구축 설정 ip 주소>/compute/v2.1‘ 를 호출해서 결과 값을 받아온다.

도출 과정

3번에서 resp 안 “catalog”에 저장된 nova 정보를 보면 “url” key 의 value 값으로 nova URI 가 저장되어있다.

결과를 이쁘게 table 형식으로 출력해주는 함수는 무엇일까요?

결론

site-packages/cliff/formatters/tables.py TableFormatter 클래스의 emit_list 메소드 에서 결과를 이쁘게 table 형식으로 출력해준다.

도출과정

server list 인자의 openstackclient/compute/v2/server.py 파일에서의 처리 결과로 column_names 와 data 값을 할당 받는다.

  • column_names=(〈ID〉, 〈Name〉, 〈Status〉, 〈Networks〉, 〈Image〉, 〈Flavor〉)
  • data = 해당 명령어 맞는 값들이 들어있는 generator 이다.
1
2
3
4
5
6
7
8
9
class DisplayCommandBase(command.Command, metaclass=abc.ABCMeta):
def run(self, **parsed_args**):
parsed_args = self._run_before_hooks(parsed_args)
self.formatter = self._formatter_plugins[parsed_args.formatter].obj
column_names, data = self.take_action(parsed_args)
column_names, data = self._run_after_hooks(parsed_args,
(column_names, data))
**self.produce_output(parsed_args, column_names, data)**
return 0

위에서 도출된 값들을 self.produce_output(parsed_args, column_names, data) 를 수행하며 결과값들이 출력이된다. 여기서 self 는 server list 인자 기준 ListServer object 를 가리킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Lister(display.DisplayCommandBase, metaclass=abc.ABCMeta):

...

def produce_output(self, parsed_args, column_names, data):

...

**self.formatter.emit_list(
columns_to_include, data, self.app.stdout, parsed_args,
)**

return 0

self.formatter.emit_list(columns_to_include, data, self.app.stdout, parsed_args,) 를 수행 시 cliff/formatters/table.py/TableFormatter 에 emit_list 메소드가 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def emit_list(self, column_names, data, stdout, parsed_args):

# column_names로 PrettyTable 객체를 생성해 x에 할당
x = prettytable.PrettyTable(
column_names,
print_empty=parsed_args.print_empty,
)
x.padding_width = 1 # 이 값을 변경해보면 table 형식이 달라진다는 것을 알 수 있다.

if data:
self.add_rows(x, column_names, data) # 데이터들이 table의 각 행에 입력된다.

min_width = 8
self._assign_max_widths(
stdout, x, int(parsed_args.max_width), min_width,
parsed_args.fit_width)

formatted = x.get_string()
stdout.write(formatted) # 결과값(테이블)을 출력해준다.
stdout.write('\n')
return

해당 메소드에서 column_names 와 data 를 테이블 형식으로 만들어 출력까지 수행한다.

맺으며..

OpenStack는 엄청 거대한 오픈소스 프로젝트이다. 이제까지 한번도 오픈소스 프로젝트를 건드려본 적이 없었다. 이번 기회에 어떻게 오픈소스 코드를 분석하며 동작원리를 파악하는지를 배웠다. 정말 인내심의 한계가 찾아왔었지만 계속 보고 또 보니 큰 그림이 그려지면서 뭐가 뭔지 파악이 되고 동작원리가 이해되기 시작했다. 정말 이 프로젝트를 하며 너무너무 많은 것들을 배우는 거 같다^_^