컨트리뷰톤 2020 - OpenStack Team 을 진행하면서 컨트리뷰톤 참여자들이 정리한 문서입니다.
VSCode에서 Remote Development 부가기능을 설치한다.
VSCode 좌측 하단에 있는 초록색 버튼을 클릭한 후 Remote-SSH: Connet to Host...를 클릭한다.
이후 ssh 연결 명령어를 통해 서버에 접속한다.
이때 서버는 root 계정 으로 접속해야한다.
ssh -i <pem 파일 경로> root@서버주소
정상적으로 원격지에 접속이 되었다면 서버에 Python 부가기능을 설치해야한다
원격지에서 작업을 진행할 OpenStackClient 경로를 선택해 열어준다.
vscode에서 디버깅을 하기위해서는 디버깅 설정파일인 .vscode/launch.json 파일을 생성해야한다.
.vscode/launch.json파일을 생성하는 방법에는 그림처럼 create a lauch.json file 버튼을 클릭후 launch.json 파일을 만드는 방법과
직접 .vscode 폴더 생성 후 launch.json 파일을 생성하는 방법이 있다.
launch.json파일의 설정 값을 다음 이미지와 같이 바꿔준다
.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "/usr/local/bin/openstack",
"args": [
"server list"
],
"console": "integratedTerminal",
"env": {
"OS_PROJECT_NAME": "demo",
"OS_TENANT_NAME": "demo",
"OS_USERNAME": "demo",
"OS_PASSWORD": "secret",
"OS_REGION_NAME": "RegionOne",
"OS_IDENTITY_API_VERSION": "3",
"OS_AUTH_TYPE": "password",
"OS_AUTH_URL": "http://127.0.0.1/identity",
"OS_VOLUME_API_VERSION": "3",
"OS_USER_DOMAIN_ID": "default",
"OS_PROJECT_DOMAIN_ID": "default",
"CINDER_VERSION": "3"
},
"justMyCode": false,
"gevent": true,
"redirectOutput": true
}
]
}
{
"python.pythonPath": "/usr/bin/python3"
}
이후 브레이크 포인트를 찍고 f5를 눌러 디버깅을 실행해 보면 정상적으로 디버깅이 되는것을 확인할 수 있다.
https://joonghyunlee.github.io/remote-debugging-cinder-1
https://www.notion.so/Remote-Debugging-with-PyCharm-for-OpenStack-ceb3c9a708a5437d84df8ba6edd8143c
root 사용자로 ssh 로그인 진행하기
$ sudo su
root@:~# vim ~/.ssh/authorized_keys
ssh-rsa 앞 까지 주석을 지워주면 됨
해당 문서는 "https://www.careyscloud.ie/openstack_kube" 페이지를 참고 하여 작성되었습니다.
시작하기 전에 시스템이 최신 소프트웨어로 업데이트되었는지 확인하고 몇 가지 종속성을 설치해야 한다.
# sudo apt-get update -y
# sudo apt-get upgrade -y
# sudo apt-get install ca-certificates git make jq nmap curl uuid-runtime -y
Github에서 openstack-helm-infra 및 openstack-helm 리포지토리를 복제
# git clone https://github.com/openstack/openstack-helm-infra.git
# git clone https://github.com/openstack/openstack-helm.git
다음 스크립트는 모두 컴퓨터의 openstack-helm 디렉토리에서 실행한다. . 이러한 각 스크립트의 내용은 문서에 설명되어 있지만 주로 Helm 차트를 사용하여 필요한 서비스를 설치한다.
첫 번째는 하나의 노드 kubernetes 클러스터를 시작하고 실행하고 Helm을 배포하여 Helm 차트를 사용하여 일부 이후 단계에서 Openstack 서비스를 배포 할 수 있도록하는 스크립트이다.
# cd openstack-helm
#./tools/deployment/developer/common/010-deploy-k8s.sh
Kubernetes 및 Helm이 성공적으로 배포되면 다음 스크립트는 python-pip를 사용하여 Openstack 및 Heat 클라이언트를 모두 설치한다.
이러한 클라이언트는 Openstack과 함께 명령 줄 인터페이스를 제공한다.
이 스크립트는 또한 모든 필수 Openstack 서비스에 대한 모든 Helm 차트를 빌드한다.
# ./tools/deployment/developer/common/020-setup-client.sh
Ingress 를 배포하는 스크립트를 실행한다.
# ./tools/deployment/developer/common/030-ingress.sh
Openstack 클라우드의 공유 스토리지의 경우 Ceph 스토리지 또는 NFS Provisioner를 사용할 수 있다. 간단하게 배포하기 위하여 NFS를 사용 하기 위하여 NFS 를 배포하기 위한 스크립트를 실행 한다.
# ./tools/deployment/developer/nfs/040-nfs-provisioner.sh
A database is required by each of the Openstack services. MariaDB is the database that is deployed in this solution
각 Openstack 서비스에 데이터베이스로 사용 할 MariaDB를 배포 한다.
# ./tools/deployment/developer/nfs/050-mariadb.sh
RabbitMQ 를 배포 한다.
# ./tools/deployment/developer/nfs/060-rabbitmq.sh
Memcached를 배포 한다.
# ./tools/deployment/developer/nfs/070-memcached.sh
다음 스크립트를 실행하여 Keystone을 배포한다. Keystone은 Openstack과 함께 제공되는 잘 알려진 ID 서비스로 사용자 및 클라우드의 다른 서비스를 인증하는 데 사용된다
# ./tools/deployment/developer/nfs/080-keystone.sh
Horizon으로도 알려진 Openstack Dashboard는 다음 스크립트를 사용하여 배포한다.
# ./tools/deployment/developer/nfs/100-horizon.sh
Glance 배포를 배포한다.
# ./tools/deployment/developer/nfs/120-glance.sh
OpenvSwitch는 모든 Openstack 배포에서 거의 표준이 된 가상 스위치로 이를 통해 사용자는 하나의 클라우드 내에서 많은 수의 가상 네트워크를 만들 수 있다.
OpenvSwitch를 아래 스크립트로 배포한다.
# ./tools/deployment/developer/nfs/140-openvswitch.sh
Glance 서비스가 가동되고 실행되면 Libvirt를 배포 할 차례이다. Libvirt는 QEMU와 인터페이스하는 데 사용된다.
# ./tools/deployment/developer/nfs/150-libvirt.sh
compute-kit 스크립트는 Nova 서비스와 Neutron 서비스를 모두 포함 한다. Nova 서비스는 Openstack의 컴퓨팅 서비스이고 Neutron은 라우터와 같은 네트워크 개체 생성을 관리하는 네트워킹 서비스이다.
# ./tools/deployment/developer/nfs/160-compute-kit.sh
공용 네트워크에 대한 게이트웨이 설정
# ./tools/deployment/developer/nfs/170-setup-gateway.sh
이 모든 과정을 마치면 이제 Openstack 대시 보드를 https : //horizon.openstack.svc.cluster.local에서 사용할 수 있다
$ openstack server list
openstack-server-list¶
$ openstack server list --long
openstack-server-list—long¶
openstack server list
의 column 정보는 어디에 있는가
openstack server list
의 output인 Table은 어떻게 출력되는가
openstack server list -c ""
은 어떻게 동작하는가
openstack server list -c ""
명령어 실행 시에 더 많은 column을
선택할 수 있어야 합니다.
openstack server list -c "C1,C2,C3"
등 ‘,’ 를 사용하여 multiple
column을 선택할 수 있어야 합니다.
openstack server list -c ""
명령어 실행 시에 선택할 수 있는 Column이 제한되어 있습니다.
openstack server list
, openstack server list --long
명령어를
통해 출력되는 table의 column만 선택할 수 있습니다. table로 출력되는
column과 선택할 수 있는 column을 분리하여 다양한 column을 선택할 수
있도록 할 예정입니다.
openstack server list -c "C1,C2,C3"
명령어 실행 시에 Error가 발생합니다.
Multiple Column 선택을 위해서는
openstack server list -c C1 -c C2 -c C3
으로 선택해야합니다. Code
Flow를 trace 해서 선택된 column이 출력되는 부분을 찾아야 합니다.
--all
: 검색할 수 있는 모든 파라미터
h-option¶
# Class ListServer :
# def get_parser():
parser.add_argument(
'--all',
action='store_true',
default=False,
help=_('List All fields in output'),
)
# Class ListServer :
# def take_action() :
elif parsed_args.all:
if parsed_args.no_name_lookup:
columns = (
'ID',
'Name',
'Status',
'Networks',
'Image ID',
'Flavor ID',
'OS-EXT-AZ:availability_zone',
'os-extended-volumes:volumes_attached',
'user_id',
'updated',
'OS-SRV-USG:launched_at',
)
else:
columns = (
'ID',
'Name',
'Status',
'Networks',
'Image Name',
'Flavor Name',
'Flavor ID',
'OS-EXT-AZ:availability_zone',
'os-extended-volumes:volumes_attached',
'user_id',
'updated',
'OS-SRV-USG:launched_at',
)
column_headers = (
'ID',
'Name',
'Status',
'Networks',
'Image',
'Flavor',
'Flavor ID',
'Availability Zone',
'Volumes',
'User ID',
'Updated',
'OS-SRV-USG:launched_at',
)
mixed_case_fields = [
'OS-EXT-AZ:availability_zone',
'OS-SRV-USG:launched_at',
]
Ouput
openstack-server-list—all¶
-c 옵션으로는 한 개의 column 밖에 볼 수가 없다.
$ openstack server list -c Name ID
openstack-server-list–c-Name,ID¶
Feedback
옵션
-c
를 연속해서 입력하면 다수의 column을 선택할 수 있다.$ openstack server list -c ID -c Name![]()
openstack-server-list–c-ID–c-Name¶
optional argument
에 all
을 추가 후 -c
옵션이 없으면 많은
column의 table이 출력
Feedback
optional argument
에--long
과--all
이 있으면 사용자에게 혼란을 줍니다.--all
옵션은 제거하는 것이 좋을 것 같습니다.
Code의 부분만 발췌하였습니다. 전체 코드가 아님을 알려드립니다.
$ openstack server list -c Name
output으로 Table이 출력되는 흐름을 trace 했습니다.
# /cliff/app.py
# full_name : 'openstack server list'
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
# openstack server list -c Name
# columns에서 'Name' 추가됨
# parsed_args :
Namespace(all=False, all_projects=False, changes_before=None, changes_since=None, columns=['Name'], deleted=False, fit_width=False, flavor=None, formatter='table', host=None, image=None, instance_name=None, ip=None, ip6=None, limit=None, locked=False, long=False, marker=None, max_width=0, name=None, name_lookup_one_by_one=False, no_name_lookup=False, noindent=False, print_empty=False, project=None, project_domain=None, quote_mode='nonnumeric', reservation_id=None, sort_columns=[], status=None, unlocked=False, user=None, user_domain=None)
# /osc_lib/command/command.py
class Command(command.Command):
def run(self, parsed_args):
print("args : ",parsed_args.__class__)
self.log.debug('run(%s)', parsed_args)
return super(Command, self).run(parsed_args)
# super(Command, self).run(parsed_args)
# -> /cliff/display.py
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
column_names, data = self.take_action(parsed_args) 결과
# openstackclient/compute/v2/server.py 의 Output
# (('ID', 'Name', 'Status', 'Networks', 'Image', 'Flavor'), <generator object ListServer.take_action.<locals>.<genexpr> at 0x7fc885014ba0>)
(Pdb) p column_names
('ID', 'Name', 'Status', 'Networks', 'Image', 'Flavor')
(Pdb) p data
<generator object ListServer.take_action.<locals>.<genexpr> at 0x7fbe731c4d00>
# super(Command, self).run(parsed_args)
# -> /cliff/lister.py
def produce_output(self, parsed_args, column_names, data):
if parsed_args.sort_columns and self.need_sort_by_cliff:
indexes = [column_names.index(c) for c in parsed_args.sort_columns
if c in column_names]
if indexes:
data = sorted(data, key=operator.itemgetter(*indexes))
(columns_to_include, selector) = self._generate_columns_and_selector(
parsed_args, column_names)
if selector:
# Generator expression to only return the parts of a row
# of data that the user has expressed interest in
# seeing. We have to convert the compress() output to a
# list so the table formatter can ask for its length.
data = (list(self._compress_iterable(row, selector))
for row in data)
self.formatter.emit_list(columns_to_include,
data,
self.app.stdout,
parsed_args,
)
return 0
(Pdb) p columns_to_include
['Name']
(Pdb) p selector
[False, True, False, False, False, False]
openstack-server-list¶
selector 변수에는 openstack server list
명령어 실행 시 출력되는
column들이 순서대로 매칭되어 있습니다.
저는 -c Name
옵션을 추가했기 때문에 Name 외의 다른 column들은
False로 되어있는 것을 볼 수 있습니다.
formatter.emit_list
def emit_list(self, column_names, data, stdout, parsed_args):
x = prettytable.PrettyTable(
column_names,
print_empty=parsed_args.print_empty,
)
x.padding_width = 1
# Add rows if data is provided
if data:
self.add_rows(x, column_names, data)
# Choose a reasonable min_width to better handle many columns on a
# narrow console. The table will overflow the console width in
# preference to wrapping columns smaller than 8 characters.
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
input parameter
(Pdb) p column_names
['Name']
(Pdb) p data
<generator object Lister.produce_output.<locals>.<genexpr> at 0x7fbe73159048>
(Pdb) p stdout
<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
(Pdb) p parsed_args
Namespace(all=False, all_projects=False, changes_before=None, changes_since=None, columns=['Name'], deleted=False, fit_width=False, flavor=None, formatter='table', host=None, image=None, instance_name=None, ip=None, ip6=None, limit=None, locked=False, long=False, marker=None, max_width=0, name=None, name_lookup_one_by_one=False, no_name_lookup=False, noindent=False, print_empty=False, project=None, project_domain=None, quote_mode='nonnumeric', reservation_id=None, sort_columns=[], status=None, unlocked=False, user=None, user_domain=None)
(Pdb) n
prettyTable.PrettyTable
명령으로 해당 column_names에 대한 Table이
생성이 되고 x.get_string()
으로 string 형태의 표가 출력됩니다.
(Pdb) p formatted
'+-----------+\n| Name |\n+-----------+\n| joostance |\n| 90000e |\n| dev |\n+-----------+'
-c
옵션이 주어졌을 때 parsed_args
에 값이 어떻게 들어오는지
trace 해봤습니다.
# /cliff/app.py
# full_name : 'openstack server list'
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
# /usr/lib/python3.6/argparse.py
# sub_argv (args) : ['-c', 'Name']
# =====================================
# Command line argument parsing methods
# =====================================
def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
return args
def parse_known_args(self, args=None, namespace=None):
# args default to the system args
if args is None:
args = _sys.argv[1:]
# default Namespace built from parser defaults
if namespace is None:
namespace = Namespace()
# add any action defaults that aren't present
for action in self._actions:
if action.dest is not SUPPRESS:
if not hasattr(namespace, action.dest):
if action.default is not SUPPRESS:
setattr(namespace, action.dest, action.default)
for 문에서 namespace 값을 setting합니다.
(Pdb) p namespace
Namespace(all=False, all_projects=False, changes_before=None, changes_since=None, columns=[], deleted=False, fit_width=False, flavor=None, formatter='table', host=None, image=None, instance_name=None, ip=None, ip6=None, limit=None, locked=False, long=False, marker=None, max_width=0, name=None, name_lookup_one_by_one=False, no_name_lookup=False, noindent=False, print_empty=False, project=None, project_domain=None, quote_mode='nonnumeric', reservation_id=None, sort_columns=[], status=None, unlocked=False, user=None, user_domain=None)
이어서 parse_known_args…
# parse the arguments and exit if there are any errors
try:
namespace, args = self._parse_known_args(args, namespace)
return namespace, args
(Pdb) p namespace
Namespace(all=False, all_projects=False, changes_before=None, changes_since=None, columns=['Name'], deleted=False, fit_width=False, flavor=None, formatter='table', host=None, image=None, instance_name=None, ip=None, ip6=None, limit=None, locked=False, long=False, marker=None, max_width=0, name=None, name_lookup_one_by_one=False, no_name_lookup=False, noindent=False, print_empty=False, project=None, project_domain=None, quote_mode='nonnumeric', reservation_id=None, sort_columns=[], status=None, unlocked=False, user=None, user_domain=None)
**_parse_known_args** 메소드 호출 후 namespace
의 columns
가
변화되었습니다. -c Name
의 Name
이 리스트에 추가되었습니다.
해당 namespace
는 parsed_args = cmd_parser.parse_args(sub_argv)
로 리턴됩니다.
** 좀 더 들어가 볼까요?
**_parse_known_args**
def _parse_known_args(self, arg_strings, namespace):
# replace arg strings that are file references
if self.fromfile_prefix_chars is not None:
arg_strings = self._read_args_from_files(arg_strings)
# find all option indices, and determine the arg_string_pattern
# which has an 'O' if there is an option at an index,
# an 'A' if there is an argument, or a '-' if there is a '--'
option_string_indices = {}
arg_string_pattern_parts = []
arg_strings_iter = iter(arg_strings)
for i, arg_string in enumerate(arg_strings_iter):
# all args after -- are non-options
if arg_string == '--':
arg_string_pattern_parts.append('-')
for arg_string in arg_strings_iter:
arg_string_pattern_parts.append('A')
# otherwise, add the arg to the arg strings
# and note the index if it was an option
else:
option_tuple = self._parse_optional(arg_string)
if option_tuple is None:
pattern = 'A'
else:
option_string_indices[i] = option_tuple
pattern = 'O'
arg_string_pattern_parts.append(pattern)
# join the pieces together to form the pattern
arg_strings_pattern = ''.join(arg_string_pattern_parts)
# converts arg strings to the appropriate and then takes the action
seen_actions = set()
seen_non_default_actions = set()
(Pdb) p arg_strings
['-c', 'Name']
(Pdb) p arg_string_pattern_parts
['O', 'A']
(Pdb) p arg_strings_pattern
'OA'
-옵션
은 ‘O’ , parameter
는 ‘A’ 로 바꿔서
arg_strings_pattern
에 저장됩니다.
[더보기]
IF openstack server list -c ID Name
(Pdb) p arg_string '-c' (Pdb) p option_tuple (_AppendAction(option_strings=['-c', '--column'], dest='columns', nargs=None, const=None, default=[], type=None, choices=None, help='specify the column(s) to include, can be repeated to show multiple columns', metavar='COLUMN'), '-c', None) (Pdb) n > /usr/lib/python3.6/argparse.py(1821)_parse_known_args() -> pattern = 'O'
(Pdb) p arg_string 'ID' (Pdb) p option_tuple None (Pdb) n > /usr/lib/python3.6/argparse.py(1818)_parse_known_args() > -> pattern = 'A'
(Pdb) p arg_string 'Name' (Pdb) p option_tuple None (Pdb) n > /usr/lib/python3.6/argparse.py(1818)_parse_known_args() > -> pattern = 'A' (Pdb) p arg_strings_pattern 'OAA'
arg_strings_pattern에서 OA, OAOA 뿐 아니라 OAA, OAAA … 도 인식하도록 구현
현재에는 -c
옵션 사용 시 O 뒤에 하나의 A만 인식하여 Namespace의
column에 넣도록 되어 있습니다.
명령어 openstack server list -c ID Name
를 실행하면
arg_strings_pattern
이 ‘OAA’ 가 됩니다.
# /usr/lib/python3.6/argparse.py(1788)_parse_known_args()
1915 start = start_index + 1
1916 selected_patterns = arg_strings_pattern[start:]
1917 arg_count = match_argument(action, selected_patterns)
1918 stop = start + arg_count
1919 args = arg_strings[start:stop]
1920 action_tuples.append((action, args, option_string))
1921 break
# /usr/lib/python3.6/argparse.py(1788)_match_argument()
2065 def _match_argument(self, action, arg_strings_pattern):
2066 # match the pattern for this action to the arg strings
2067 -> nargs_pattern = self._get_nargs_pattern(action)
2068 match = _re.match(nargs_pattern, arg_strings_pattern)
def _get_nargs_pattern(self, action):
# in all examples below, we have to allow for '--' args
# which are represented as '-' in the pattern
nargs = action.nargs
# the default (None) is assumed to be a single argument
if nargs is None:
nargs_pattern = '(-*A-*)'
# allow zero or one arguments
elif nargs == OPTIONAL:
nargs_pattern = '(-*A?-*)'
# allow zero or more arguments
elif nargs == ZERO_OR_MORE:
nargs_pattern = '(-*[A-]*)'
기존에서는 action.nargs
가 None
으로 설정이 되어서
nargs_pattern이 (-*A-*)
가 되었습니다.
그래서 arg_strings_pattern
에서 하나의 ‘O’ 는 하나의 ‘A’ 만
인식하였던 것입니다.
class _AppendAction(Action):
def __init__(self,
option_strings,
dest,
nargs=ZERO_OR_MORE,
const=None,
default=None,
type=None,
choices=None,
required=False,
help=None,
metavar=None):
_AppendAction
클래스의 nargs
변수를 None
에서
ZERO_OR_MORE ('*')
로 변경합니다. 이렇게 함으로써 ‘OAA’ , ‘OAAA’ 등
하나의 ‘O’ 가 다수의 ‘A’ 를 인식할 수 있도록 하였습니다.
_get_nargs_pattern
함수에서 action.nargs
가 ZERO_OR_MORE
이기 때문에 최종적인 nargs_pattern
이 '([A]*)
가 됩니다.
[더보기]
nargs_pattern과 arg_strings_pattern의 매칭
> /usr/lib/python3.6/argparse.py(2071)_match_argument() -> if match is None: (Pdb) l 2066 # match the pattern for this action to the arg strings 2067 nargs_pattern = self._get_nargs_pattern(action) 2068 match = _re.match(nargs_pattern, arg_strings_pattern
> /usr/lib/python3.6/re.py(169)match() -> def match(pattern, string, flags=0): (Pdb) l 164 error = sre_compile.error 165 166 # -------------------------------------------------------------------- 167 # public interface 168 169 -> def match(pattern, string, flags=0): 170 """Try to apply the pattern at the start of the string, returning 171 a match object, or None if no match was found.""" 172 return _compile(pattern, flags).match(string)
(Pdb) p pattern '([A]*)' (Pdb) p string 'AA' (Pdb) p match <_sre.SRE_Match object; span=(0, 2), match='AA'>
Namespace의 columns에 리스트로 들어가는 오류 해결
문제
$ openstack server list -c ID Name
No recognized column names in [['ID', 'Name']]. Recognized columns are ('ID', 'Name', ...).
parse_known_args
함수에서 Namespace의 columns 값에 [‘ID’, ‘Name’] 이
들어가 있어서 이런 오류가 발생하였습니다.
Namespace(all_projects=False, changes_before=None, changes_since=None, columns=[['ID', 'Name']], deleted=False, fit_width=False, flavor=None, formatter='table', host=None, image=None, instance_name=None, ip=None, ip6=None, limit=None, locked=False, long=False, marker=None, max_width=0, name=None, name_lookup_one_by_one=False, no_name_lookup=False, noindent=False, print_empty=False, project=None, project_domain=None, quote_mode='nonnumeric', reservation_id=None, sort_columns=[], status=None, unlocked=False, user=None, user_domain=None)
해결
class _AppendAction(Action):
def __call__(self, parser, namespace, values, option_string=None):
items = _copy.copy(_ensure_value(namespace, self.dest, []))
# items.append(values)
for value in values:
items.append(value)
setattr(namespace, self.dest, items)
(Pdb) p values
['ID', 'Name']
기존에는 _AppendAction
클래스의 nargs가 None
이였기 때문에
values가 문자열이였습니다.
하지만 nargs를 ZERO_OR_MORE
로 수정했기 때문에 values에 리스트가
됩니다.
그래서 리스트를 순회하여 요소들을 items에 넣어줘 해당 오류를 해결하였습니다.
결론
openstack-server-list-multi-column-by-one-opt¶
멘토님의 피드백대로 --all
옵션은 삭제하였습니다.
-c
옵션으로 column을 선택할 때, column은 출력되는 column 중에서
선택해야만 합니다. 하지만 본 이슈는 -c
옵션을 통해 더 많은 column을
선택할 수 있어야 합니다. 그래서 -c
옵션이 없을 때에는 기존과 같이
동작을 하고, -c
옵션이 있다면 모든 출력할 수 있는 정보를 column과
column_headers에 저장하도록 구현했습니다. 이를 통해 -c
옵션으로
선택할 수 있는 column의 수를 늘렸습니다.
columns = (
'ID',
'Name',
'Status',
'Networks',
'Image Name',
'Flavor Name',
'Flavor ID',
'OS-DCF:diskConfig',
'OS-EXT-AZ:availability_zone',
'OS-EXT-SRV-ATTR:host',
'OS-EXT-SRV-ATTR:hypervisor_hostname',
'OS-EXT-SRV-ATTR:instance_name',
'OS-EXT-STS:power_state',
'OS-EXT-STS:task_state',
'OS-EXT-STS:vm_state',
'OS-SRV-USG:launched_at',
'OS-SRV-USG:terminated_at',
'user_id',
'project_id',
'accessIPv4',
'accessIPv6',
'config_drive',
'created',
'hostId',
'key_name',
'progress',
'security_groups',
'status',
'updated',
'os-extended-volumes:volumes_attached',
'properties',
)
column_headers = (
'ID',
'Name',
'Status',
'Networks',
'Image',
'Flavor',
'Flavor ID',
'Disk Config',
'Availability Zone',
'Host',
'Hypervisor Hostname',
'Instance Name',
'Power State',
'Task State',
'Vm State',
'Launched At',
'Terminated At',
'User ID',
'Project ID',
'AccessIPv4',
'AccessIPv6',
'Config Drive',
'Created',
'Host ID',
'Key Name',
'Progress',
'Security Groups',
'Status',
'Updated',
'Volumes',
'Properties',
)
mixed_case_fields = [
'OS-DCF:diskConfig',
'OS-EXT-AZ:availability_zone',
'OS-EXT-SRV-ATTR:host',
'OS-EXT-SRV-ATTR:hypervisor_hostname',
'OS-EXT-SRV-ATTR:instance_name',
'OS-EXT-STS:power_state',
'OS-EXT-STS:task_state',
'OS-EXT-STS:vm_state',
'OS-SRV-USG:launched_at',
'OS-SRV-USG:terminated_at',
]
-c
옵션으로 선택할 수 있는 column은
openstack server show [instanceName]
로 출력되는 정보와 동일합니다.
openstack server list
을 실행하면 ListServer
클래스의
take_action
메소드가 실행이 되고, server들의 정보를 가져옵니다. 이
부분을 살펴보겠습니다.
class ListServer(command.Lister):
def take_action(self, parsed_args):
data = compute_client.servers.list(search_opts=search_opts,
marker=marker_id,
limit=parsed_args.limit)
compute_client.servers.list
를 통해 server들의 정보를 가져옵니다.
dir()
내장 함수는 어떤 객체를 인자로 넣어주면 해당 객체가 어떤
변수와 메소드를 가지고 있는지 나열해줍니다.
참고
아래에서는 필요한 data 외는 삭제하여 포스팅하였습니다.
(Pdb) p data
[<Server: joostance>, <Server: 90000e>, <Server: dev>]
(Pdb) p data[0].__class__
<class 'novaclient.v2.servers.Server'>
(Pdb) p dir(data[0])
['HUMAN_ID', 'NAME_ATTR', 'OS-DCF:diskConfig', 'OS-EXT-AZ:availability_zone', 'OS-EXT-SRV-ATTR:host', 'OS-EXT-SRV-ATTR:hypervisor_hostname', 'OS-EXT-SRV-ATTR:instance_name', 'OS-EXT-STS:power_state', 'OS-EXT-STS:task_state', 'OS-EXT-STS:vm_state', 'OS-SRV-USG:launched_at', 'OS-SRV-USG:terminated_at','accessIPv4', 'accessIPv6','addresses', 'api_version', 'config_drive', 'created', 'flavor', 'hostId', 'human_id', 'id', 'image', 'key_name', 'name', 'networks', 'os-extended-volumes:volumes_attached', 'progress', 'request_ids', 'security_groups', 'tag_list', 'tenant_id', 'updated', 'user_id', 'x_openstack_request_ids']
compute_client.servers.list
를 통해 가져온 server의 정보는
novaclient.v2.servers.Server
클래스인 것을 알 수 있고, dir() 함수를
통해 변수들을 볼 수 있습니다. 하지만 해당 객체에는 project_id
나
properties
정보는 가지고 있지 않습니다.
openstack server show [instanceName]
명령어에서는 project_id
나
properties
도 확인 할 수 있으므로, 해당 명령어에서는 해당 data를
어떻게 가져오는지 알아봤습니다.
openstack server show [instanceName]
명령어는 ShowServer
의
take_action
메소드를 사용하여 server의 data를 가져옵니다.
class ShowServer(command.ShowOne):
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
server = utils.find_resource(compute_client.servers,
parsed_args.server)
if parsed_args.diagnostics:
(resp, data) = server.diagnostics()
if not resp.status_code == 200:
self.app.stderr.write(_(
"Error retrieving diagnostics data\n"
))
return ({}, {})
else:
data = _prep_server_detail(compute_client,
self.app.client_manager.image, server,
refresh=False)
return zip(*sorted(data.items()))
openstack server show dev
명령어를 실행한 뒤 data를 출력한
모습입니다. 해당 data에는 project_id
와 properties
가
존재합니다.
(Pdb) p data
{'id': '0c2ceb17-4333-484f-9158-88da3c8ebd67', 'name': 'dev', 'status': 'ACTIVE', 'user_id': '413672a5e28b4dbb9a0dbc5285bacac9', 'hostId': '4f15df15234d5425e5f82d3402c3dcbb92348c531adce1cf080273ca', 'image': '', 'flavor': 'm1.nano (42)', 'created': '2020-08-12T16:38:49Z', 'updated': '2020-08-12T16:39:04Z', 'addresses': 'private=fd2f:eb32:c341:0:f816:3eff:fe75:8179, 10.0.0.53', 'accessIPv4': '', 'accessIPv6': '', 'OS-DCF:diskConfig': 'AUTO', 'progress': 0, 'OS-EXT-AZ:availability_zone': 'nova', 'config_drive': '', 'key_name': None, 'OS-SRV-USG:launched_at': '2020-08-12T16:39:04.000000', 'OS-SRV-USG:terminated_at': None, 'OS-EXT-SRV-ATTR:host': 'ubuntu', 'OS-EXT-SRV-ATTR:instance_name': 'instance-00000001', 'OS-EXT-SRV-ATTR:hypervisor_hostname': 'ubuntu', 'OS-EXT-STS:task_state': None, 'OS-EXT-STS:vm_state': 'active', 'OS-EXT-STS:power_state': 'Running', 'volumes_attached': "id='8a88230c-a821-4685-9afc-6eab20286ebd'", 'security_groups': "name='default'", 'properties': '', 'project_id': '4e37ed3493c24ba18dd7844c5e925bfb'}
이를 통해 _prep_server_detail
메소드를 통해 project_id
와
properties
를 가져올 수 있는 것을 알았습니다.
그래서 ListServer
의 take_action
메소드에서 server들의 data를
가져올 때, _prep_server_detail
메소드를 통해 project_id
와
properties
를 가져오도록 구현하였습니다.
# class ListServer 의 def take_action
for s in data:
more_data = _prep_server_detail(compute_client,
image_client, s,
refresh=False)
s.project_id = more_data["project_id"]
s.properties = more_data["properties"]
pdb는 The Python Debugger로 Python 프로그램을 위한 대화형 소스 코드 디버거입니다.
소스 라인 레벨에서 breakpoint 을 설정하여 디버깅할 수 있습니다.
Lib를 import 합니다.
import pdb
Breakpoint 설정할 코드 라인에 다음 명령어를 입력합니다.
pdb.set_trace()
Pdb를 실행합니다.
$ python3 -m pdb 파일-이름.py
Ex
stack@ubuntu:~/devstack$ python3 -m pdb /usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py
> /usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py(16)<module>()
-> """Compute v2 Server action implementations"""
(Pdb) list
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
13 # under the License.
14 #
15
16 -> """Compute v2 Server action implementations"""
17
18 import argparse
19 import getpass
20 import io
21 import logging
** 저는 openstack server list
명령어 입력 후 Class ListServer
에서의 Code Flow를 보기 위해 해당 Class 안에 breakpoint를 설정하였고,
openstack server list
를 실행하였습니다. 이렇게 되면 첫번째
breakpoint인 pdb.set_trace()
라인에 멈춥니다.
(다음) breakpoint로 이동합니다.
(Pdb) continue
continue
는 다음 breakpoint로 이동합니다. 단축어로 c
를
입력해도 됩니다.
Ex
(Pdb) continue
> /usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py(1142)ListServer()
-> _description = _("List servers")
(Pdb) list
1137 raise SystemExit
1138
1139
1140 class ListServer(command.Lister):
1141 pdb.set_trace()
1142 -> _description = _("List servers")
1143
1144 def get_parser(self, prog_name):
1145 parser = super(ListServer, self).get_parser(prog_name)
1146 parser.add_argument(
1147 '--reservation-id',
server.py의 16 Line에서 breakpoint 다음 Line인 1142 Line으로 이동했습니다.
launchpad id : hanjoo
zanata id : hanjoo96
stackanalytics pr : https://github.com/stackalytics/default_data/pull/222
첫 컨트리뷰트가 될 이슈를 이것 으로 결정했다.
openstack server create
명령어로 인스턴스를 생성할 때, --image-property 옵션을 사용할 수 있었다.
property라는 단어가 openstack image show
로 이미지 정보를 볼 때 나오는 properties와 관련이 있다고 생각해서 cirros 이미지를 찾아보았다.
$ openstack image show cirros-0.5.1-x86_64-disk
+------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Field | Value |
+------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| checksum | 1d3062cd89af34e419f7100277f38b2b |
| container_format | bare |
| created_at | 2020-08-15T12:28:18Z |
| disk_format | qcow2 |
| file | /v2/images/56aad641-dd16-44af-86f0-b61ec980709c/file |
| id | 56aad641-dd16-44af-86f0-b61ec980709c |
| min_disk | 0 |
| min_ram | 0 |
| name | cirros-0.5.1-x86_64-disk |
| owner | e90e398ff160422b84557c3924f775fe |
| properties | hw_rng_model='virtio', os_hash_algo='sha512', os_hash_value='553d220ed58cfee7dafe003c446a9f197ab5edf8ffc09396c74187cf83873c877e7ae041cb80f3b91489acf687183adcd689b53b38e3ddd22e627e7f98a09c46', os_hidden='False', os_version='0.5.1', owner_specified.openstack.md5='', owner_specified.openstack.object='images/cirros-0.5.1-x86_64-disk', owner_specified.openstack.sha256='' |
| protected | False |
| schema | /v2/schemas/image |
| size | 16338944 |
| status | active |
| tags | |
| updated_at | 2020-08-15T12:33:37Z |
| visibility | public |
+------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
이 결과에서 나오는 프로퍼티를 사용해 VM을 생성하는 방식을 여러 번 시도했다. 여기서 한 가지 문제점을 발견했다.
이미지가 여러 개 있을 때, 프로퍼티로 이미지를 선택하는 것이 제대로 동작하지 않는 것을 발견했다. 사용한 명령어는 다음과 같다.
$ openstack image set --os-version 0.5.1 cirros-0.5.1-x86_64-disk
$ openstack image save cirros-0.5.1-x86_64-disk --file cirros.image
$ openstack image create cirros --file cirros.image
+------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| Field | Value |
+------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
| container_format | bare |
| created_at | 2020-08-17T10:48:18Z |
| disk_format | raw |
| file | /v2/images/53cbd213-a39c-4d88-b8e1-5e14a512e292/file |
| id | 53cbd213-a39c-4d88-b8e1-5e14a512e292 |
| min_disk | 0 |
| min_ram | 0 |
| name | cirros |
| owner | e1c2fb4e069e4a5a8935bef43df00e85 |
| properties | os_hidden='False', owner_specified.openstack.md5='', owner_specified.openstack.object='images/cirros', owner_specified.openstack.sha256='' |
| protected | False |
| schema | /v2/schemas/image |
| status | queued |
| tags | |
| updated_at | 2020-08-17T10:48:18Z |
| visibility | shared |
+------------------+--------------------------------------------------------------------------------------------------------------------------------------------+
$ openstack server create cirros-test \
--flavor 1 \
--key-name key \
--image-property os_version=0.5.1
No images match the property expected by --image-property
위의 명령어들을 요약하면 다음과 같다.
기존 이미지(cirros-0.5.1-x86_64-disk)에 os_version
이라는 프로퍼티를 설정한다.
기존 이미지를 저장한 뒤, 그 파일로 새로운 이미지를 등록한다.
서버를 생성할 때 --image-property에 os_version
프로퍼티를 사용한다.
분명히 os_version
을 설정했음에도 불구하고 이미지를 선택하지 못하는 것을 확인할 수 있었다.
발견한 문제점을 토대로 코드를 분석했다. 분석해야 할 파일은 python-openstackclient 프로젝트에서 openstackclient/compute/v2/server.py 파일이며, VM을 생성하는 함수는 CreateServer
클래스의 take_action()
이다.
CreateServer
클래스의 take_action()
를 살펴보니, _match_image()
라는 함수가 take_action()
내부에 정의되어 있었고, 여기서 --image-propert 옵션과 관련된 로직이 실행되고 있었다. 해당 함수는 아래와 같은 모습이다.
def _match_image(image_api, wanted_properties):
image_list = image_api.images()
images_matched = []
for img in image_list:
img_dict = {}
# exclude any unhashable entries
for key, value in img.items():
try:
set([key, value])
except TypeError:
pass
else:
img_dict[key] = value
if all(k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()):
images_matched.append(img)
else:
return []
return images_matched
이 함수의 문제점은 --image-property로 명시한 조건과 일치하지 않는 이미지가 존재하면 필터링된 이미지 리스트(위 함수에서는 images_matched
)가 아닌 빈 리스트를 리턴한다는 것이다. 이렇게 되면 --image-property 조건과 일치하는 이미지가 있어도 무시될 가능성이 존재하며, 그 경우가 바로 위에서 재현된 버그와 같은 것이다.
이 문제는 빈 리스트를 반환하는 코드 두 줄을 제거하여 수정했다.
def _match_image(image_api, wanted_properties):
image_list = image_api.images()
images_matched = []
for img in image_list:
img_dict = {}
# exclude any unhashable entries
for key, value in img.items():
try:
set([key, value])
except TypeError:
pass
else:
img_dict[key] = value
if all(k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()):
images_matched.append(img)
return images_matched
이슈를 올렸던 작성자가 직접 리뷰 도 올린 것을 뒤늦게 확인했다. 코드를 살펴보니 내가 발견한 것과는 다른 문제점을 발견한 것을 알게 되었다.
예를 들면, 이미지 프로퍼티 중 owner_specified.openstack.object
라는 키를 --image-property 조건으로 넣으면 이미지가 생성되지 않는다.
$ openstack image list
+--------------------------------------+--------------------------+--------+
| ID | Name | Status |
+--------------------------------------+--------------------------+--------+
| 56aad641-dd16-44af-86f0-b61ec980709c | cirros-0.5.1-x86_64-disk | active |
+--------------------------------------+--------------------------+--------+
$ openstack server create --flavor 1 --key-name key --image-property owner_specified.openstack.object=images/cirros-0.5.1-x86_64-disk --network private cirros-test
No images match the property expected by --image-property
이전과는 달리, 이미지가 하나만 있는데도 서버 생성에 실패했다. 왜 이런 문제가 발생하는지 알아보기 위해 다시 디버깅을 시도했다.
def _match_image(image_api, wanted_properties):
image_list = image_api.images()
images_matched = []
for img in image_list:
img_dict = {}
# exclude any unhashable entries
for key, value in img.items():
try:
set([key, value])
except TypeError:
pass
else:
img_dict[key] = value
if all(k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()):
images_matched.append(img)
return images_matched
이 코드에서, img
오브젝트는 이미지의 여러 프로퍼티를 저장한 딕셔너리였다. 딕셔너리의 키를 순회하면서 하나의 set으로 만들 수 있는 것만 --image-property에 사용할 수 있는 키 값의 대상이 되는 것이었다.
그런데 앞서 사용한 owner_specified.openstack.object
프로퍼티는 img
의 properties
라는 키에 딕셔너리로 저장된 값 중 하나였다. 따라서 properties
라는 키에 저장된 프로퍼티는 --image-property 필터에 사용할 수 없었던 것이다.
두 가지 경우 모두 --image-property가 제대로 동작하지 않는 원인이기 떄문에 둘 다 수정할 필요가 있었다. 내가 수정한 코드는 다음과 같다.
def _match_image(image_api, wanted_properties):
image_list = image_api.images()
images_matched = []
for img in image_list:
img_dict = {}
# exclude any unhashable entries
img_dict_items = list(img.items())
if img.properties:
img_dict_items.extend(list(img.properties.items()))
for key, value in img_dict_items:
try:
set([key, value])
except TypeError:
pass
else:
img_dict[key] = value
if all(k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()):
images_matched.append(img)
return images_matched
그리고 이 두 가지 문제점을 포함한 테스트 케이스 하나를 작성한 다음, gerrit에 리뷰를 작성했다.
사실 내가 올린 리뷰보다 먼저 생성된 리뷰 가 있었다. 멘토님이 내 리뷰를 보시고 다른 리뷰와 차이점을 언급해 주셨고, 이슈 오너가 이에 반응했다.
이슈 오너는 내 리뷰가 어떤 내용인지 잘 모르는 것 같아 코멘트를 달아 주었다. 먼저 올라간 리뷰는 코드 상 properties 키의 내용을 참조하지 않는다는 문제를 해결했다면, 내 리뷰는 이미지가 여러 개 있을 때 발생하는 문제에 관한 패치였다.
물론 내 코드에도 properties 문제를 해결하는 코드가 있지만, 먼저 올라간 리뷰가 이것을 더 잘 처리한 것 같았다. 코멘트에 네 것이 더 낫다는 말과 함께, 두 리뷰가 같이 머지되어야 스토리를 닫을 수 있다고 적었다.
그는 내가 한 말을 이해하고, 이슈를 메일링 리스트와 IRC에 올려서 같이 머지하자고 말해주었다.
리뷰를 처음 올린 8월 16일로부터 거의 20일 가까이 코드 리뷰를 받지 못하고 있었다.
Mailing List에 코드 리뷰를 부탁하는 메일을 작성하고, 그래도 답이 없다면 IRC에서 직접 논의를 이어 갈 예정이다.
Myeong Chul Chae <rncchae@gmail.com>
오후 8:30 (5분 전)
openstack-discuss@lists.openstack.org에게
Hi.
I researched the story 'openstack CLI - Create an instance using
--image-property filtering not working' and modified the code to
solve it.
This is the issue that I opened. - Link
And the hyperlink of the story is here.
In addition, there is a review posted before my review of the same
story, so conflict resolution is necessary.
Please check the commit message and history of the two reviews
and continue the discussion.
Mailing List를 작성한게 효과가 있었는지, 다음 날 바로 코드 리뷰를 받았다.
리뷰어는 내 리뷰를 보기 전에 먼저 올라온 리뷰에 코멘트를 남겼다.
두 리뷰를 비교하면서 내가 수정한 부분이 좀 더 좋다는 평을 받았다.
그러면서 내 리뷰에는 코드를 제거한 부분이 있는데 왜 그렇게 했는지 설명해달라고 요청하는 코멘트를 남겼다.
해당 코드 라인을 제거한 이유는 이미지가 여러 개 있을 때 조건에 맞는 이미지가 있어도 빈 리스트가 반환될 수 있는 문제 때문이었다.
이 내용을 설명하는 코멘트를 달아 주었다.
If any image does not fit a given condition (--image-property), an empty list is returned.
If there is an image that meets the conditions, I think it is the right behavior of this method to put it on the list and return it.
So I'm suggesting that this line should be removed.
이 코멘트를 봤는지, 리뷰어가 다음 날 다른 쪽 리뷰에 내 코드가 더 좋다는 의견을 남겼다.
멘토님을 포함한 다른 리뷰어들이 내가 올린 리뷰를 선택해서 그런지 먼저 리뷰를 올렸던 컨트리뷰터가 내 리뷰로 논의를 이어가자고 양보해주었다.
이전 리뷰의 리뷰어 중 한 명이 try-except
구문에 로그를 추가하자고 제안했다.
이 제안은 다른 리뷰에 올라온 것이지만, 내 리뷰에서 계속해서 반영해달라는 요청을 받았다.
수정이 필요한 코드는 아래와 같다.
for key, value in img.items():
try:
set([key, value])
except TypeError:
pass
else:
img_dict[key] = value
except TypeError
부분에서, 어떤 이유로 에러가 발생했는지 로그를 추가하는 것이 이번 패치의 목표였다.
위의 For 문은 --image-property
의 값과 이미지가 가지고 있는 프로퍼티의 값을 비교하기 위한 사전 작업이다. set()
함수를 사용해 ==
연산을 사용할 수 있는 프로퍼티만을 골라내는 작업을 하게 된다.
만약 ==
연산자를 사용할 수 없는(해싱 불가능한) 값이 포함되어 있다면 except
로 넘어가게 된다.
except
로 흐름이 넘어가게 되는 값의 특성이 위와 같으므로, '비교가 불가능하기 때문에 프로퍼티를 생략한다' 라는 뉘앙스를 가진 로그 문장을 작성해서 커밋했다.
바뀐 코드는 다음과 같다.
def _match_image(image_api, wanted_properties):
image_list = image_api.images()
images_matched = []
for img in image_list:
img_dict = {}
# exclude any unhashable entries
img_dict_items = list(img.items())
if img.properties:
img_dict_items.extend(list(img.properties.items()))
for key, value in img_dict_items:
try:
set([key, value])
except TypeError:
if key != 'properties':
LOG.debug('Skipped the \'%s\' attribute. '
'That cannot be compared. '
'(image: %s, value: %s)',
key, img.id, value)
pass
else:
img_dict[key] = value
if all(k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()):
images_matched.append(img)
내가 올린 패치에 다른 리뷰어가 이어서 패치를 했기 때문에, git pull
처럼 패치를 로컬 git 레포지토리에 합칠 필요가 있었다.
그러나 gerrit은 GitHub와는 다른 코드 리뷰 방식을 사용하고 있기 때문에, 본능적으로 git pull
로 패치를 받으면 안될 것 같다는 느낌을 받았다.
그래서 검색해본 결과, 다음의 StackOverflow 문서 를 찾게 되었다.
거기에는 checkout
커맨드를 사용하라는 조언이 적혀있었다.
무엇을 사용해야 되는지 알게 되었으니, 패치를 받는 일은 매우 간단했다.
리뷰의 우측 상단에 Download 라는 메뉴가 있는데, 거기에서 Checkout 이라고 쓰인 커맨드를 복사해 붙여넣기만 하면 패치가 완료된다.
정리해보면 다음과 같다.
review 페이지의 Download 항목에서 Checkout 커맨드를 복사해 로컬 Git 레포지토리에서 실행
코드 수정 작업
git commit --amend
로 커밋
git review
로 작업 업로드
stackalytics 저장소 를 Fork한다.
그런 다음, default_data.json에 자기 자신의 user 정보를 넣어야 한다.
User 정보는 launchpad id, ldap id, github id 순서대로 넣어야 한다.
계정 정보 예시는 다음과 같다.
{
"launchpad_id": "cocahack",
"github_id": "cocahack",
"zanata_id": "jake_chae",
"companies": [
{
"company_name": "*independent",
"end_date": null
}
],
"user_name": "Myeongchul Chae",
"emails": ["cocahack@naver.com", "rncchae@gmail.com"]
}
계정 정보를 채웠다면 tox를 실행해서 정보를 잘 기입했는지 확인하면 된다.
오픈스택에서 api를 요청 할 경우 여러가지 방법이 있음, 그 중에서도 python requests 모듈을 통해 토큰을 발급하고 api를 요청하는 과정에서 이상한 부분을 발견하여 문서로 작성
오픈스택 keystone에서 토큰을 활용할 수 있는 방안은 다음과 같다 keystone api
{ "auth": { "identity": { "methods": [ "password" ], "password": { "user": { "id": "ee4dfb6e5540447cb3741905149d9b6e", "password": "devstacker" } } }, "scope": { "system": { "all": true } } } }
위 json은 openstack에서 keystone을 통해서 token 발행을 할 경우 사용되는 json body정보임 위 방식으로 api를 요청 했을 경우 nova, neutron, cinder, keystone 등 api 요청을 했을 때 정상적으로 결과물을 받아 올 수 있는걸 확인함
http://controller:8776/v3/7699fd0d3e5b44fe8871af7bad08df21/volumes/detail {'X-Auth-Token': 'gAAAAABfLaJUuts0vAH_UsYEDT8QFN2X0jR3yL75R__UKNOZqo0jUNQHPSh1GduYfgl_6KxbuME-3pPSIj4h9k76wgh- Old1dBl81vpPLOc-9GdRY5E6xUBSXQM3a4IscdkmEzpSkys9bitQQDo3yTUPSdndkDEzGg'} {'badRequest': {'message': 'Malformed request url', 'code': 400}}
그러나 cinder의 경우에는 api요청을 했을 때 400에러가 나오는것을 확인 하였음 이러한 400 error는 cinder, trove, swift, maila 등의 서비스에서 위와 같은 똑같은 상황이 나타나는것을 확인 함
curl -g -i -X GET http://controller:8776/v3/7699fd0d3e5b44fe8871af7bad08df21/volumes/detail -H "Accept: application/json" -H "OpenStack-API-Version: volume 3.59" -H "User-Agent: python-cinderclient" -H "X-Auth- Token: {SHA256}1dc35186f7e05f9a211214a9da71e4408c254c1bc2a1b745c085bdc2091b482a"
위 요청은 openstack volume list --debug 명령어를 통해 디버깅을 했을 때 cinder가 api 요청을 어떻게 수행하는지 확인 하였고 정상적인 결과물이 출력되는 것을 확인 할 수 있음
또한 token에 대한 권한 확인을 하기 위해서 openstack token issue --debug를 통해 일반적은 token은 어떻게 발급하는지 확인 함
Using parameters {'username': 'admin', 'project_name': 'admin', 'user_domain_name': 'Default', 'auth_url': 'http://controller:5000/v3', 'password': '***', 'project_domain_name': 'Default'}
위와 같은 방식으로 token을 발급하는 것 확인 하엿고 이러한 디버깅을 통해서 테스트 코드를 작성
import requests def test_create_credentials_token(key, mode) : #토큰 생성 print("Create Credentials Token") token = "" if mode == 1 : data = '{"auth":{"identity":{"methods":["password"],"password":{"user": {"id":"'+key['OS_ADMIN_ID']+'","password":"'+key['OS_PASSWORD']+'"}}},"scope":{"project": {"id":"'+key['OS_PROJECT']+'"}}}}' elif mode == 2 : data = '{"auth":{"identity":{"methods":["password"],"password":{"user": {"id":"'+key['OS_ADMIN_ID']+'","password":"'+key['OS_PASSWORD']+'"}}},"scope":{"system":{"all":true}}}}' res = requests.post(key['OS_AUTH_URL']+'identity/v3/auth/tokens', data=data) return res.headers['X-Subject-token'] def set_api(OS_TOKEN, URL) : # 서비스로 api 전송 headers = { 'X-Auth-Token': OS_TOKEN, } response = requests.get(URL, headers=headers) return response if __name__ == "__main__": key = { "OS_ADMIN_ID" : "b76cd03ce71446e79fa47b707397e9a2", # admin id == openstack user list "OS_PASSWORD" : "ADMIN_PASS",# 설치 시 입력 했던 passwd "OS_PROJECT" : "1fb596f7cb454ec9a7d6533af8ce1826", # demo project id == openstack project list / admin project로 해도 상관 없음 "OS_AUTH_URL" : "http://192.168.1.8/", # 설치 시 입력했던 ip ex) 192.168.1.8/ } token = test_create_credentials_token(key,1) print("scope : project : ", token) result = set_api(token, key['OS_AUTH_URL']+"volume/v3/"+key['OS_PROJECT']+"/volumes/detail") print(result.json()) token = test_create_credentials_token(key, 2) print("2 : ", token) result = set_api(token, key['OS_AUTH_URL']+"volume/v3/"+key['OS_PROJECT']+"/volumes/detail") print(result.json())
테스트 코드 작성
Create Credentials Token scope : project : gAAAAABfMlP3R8cKFu6PynJyNatvlHdKBI0EwH7OYpqIQ_Mm4pPUu5GRGZTwGrVeoG2yzU- 5QlJB6aluIsEUAhQJ_5G7S1Jx1hh8V3CefFvo0oTbpi8NToh3LdgMaHEuThWOoPKkVFvJkJolVXEPjvSylcKIcfJimBdwai_cUX9e0w4c4encyI8 Create Credentials Token 2 : gAAAAABfMlP4EkqICPEl5JhGF8qZ9WkBuCBo0ht8XB9NTBvTPNFmD6gU3rolJEVecg5byzi_3PtorWhYuPmBJ9W8QwPpV9ZQP769zeDBRdS3B9SWlLTNgTU7PRbhIFF4RDh32Rcvb3BL65pSif6-cSq7PpAxE2rbdw {'badRequest': {'code': 400, 'message': 'Malformed request url'}}
위와 같이 1번째 project를 넣은 경우엔느 정상적으로 잘 생행 되었으며 2번째 경우는 400 error가 마찬가지로 떨어지는것을 확인 함
원인 분석
원인은 cinder git 에서 확인 할 수 있음
project_id = action_args.pop("project_id", None) context = request.environ.get('cinder.context') if (context and project_id and (project_id != context.project_id)): msg = _("Malformed request url") return Fault(webob.exc.HTTPBadRequest(explanation=msg))
여기서 keystone / neutron / nova 등은 api Endpoint URI에 project id가 포함된게 없어서 그 if 문에서 검사하는 조건들이 모두 None임 그러나 cinder 는 api Endpoint uri 에 project id가 있기 때문에 context.project_id = None 인데. argument로 넘어온 project_id는 uri에 있는 project id 라서 if문에서 false가 됨 그럼 왜 context.project_id가 None인가? -> 토큰의 scope가 프로젝트 단위가 아니라 system이니까.. 당연히 지금 토큰으로 처리하는 요청은 project id가 없으므로 400에러 발생
이러한 이슈는 2018년 버그 리포팅에도 올라왔음 버그
그에 따라서 문서를 추적해봤을 때
The authorization scope, including the system (Since v3.10), a project, or a domain (Since v3.4). If multiple scopes are specified in the same request (e.g. project and domain or domain and system) an HTTP 400 Bad Request will be returned, as a token cannot be simultaneously scoped to multiple authorization targets. An ID is sufficient to uniquely identify a project but if a project is specified by name, then the domain of the project must also be specified in order to uniquely identify the project by name. A domain scope may be specified by either the domain’s ID or name with equivalent results.
위와 같은 방법을 사용 시 400 에러가 발생한다고 나와있음 추가적으로 이러한 이슈를 해결해보기 위해서 메일링 리스트를 작성
Message: 1 Date: Sun, 23 Aug 2020 21:18:32 +0900 From: Mingi Jo <jomin0613@gmail.com> To: openstack-discuss@lists.openstack.org Subject: [keystone] openstack token auth scpore system Question Hi, I'm studying OpenStack.If you use OpenStack and use it with a keystone token on all computers,If there is a project in the endpoint URL, the api request cannot be made properly.The error message is output at 400, and the request fails. We've looked into this, and I've found out,https://bugs.launchpad.net/cinder/+bug/1745905Here's the bug reporting, and I think it's done with the paperwork.However, various services such as cinder, swift, and probe are required to include projects in the endpoint url of the installation guide, which is considered contradictory.Is there any way to fix this? -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.openstack.org/pipermail/openstack-discuss/attachments/20200823/5f1612ec/attachment-0001.html>
이메일을 작성하였고 명확한 답변은 아직 안온상태
2020-09-11 추가 http://eavesdrop.openstack.org/meetings/keystone/2020/keystone.2020-09-08-16.58.log.html
keystone 미팅에 참석하여 어떻게 문제사항을 수정해야할지 힌트를 얻어 추가적인 코드리뷰가 가능할 것으로 보임
launchpad id : ddorahee123123123
zanata id : https://translate.openstack.org/dashboard/?dswid=-7546
stackanalytics pr : https://github.com/stackalytics/default_data/pull/208
이 가이드는 OpenStack 컨트리뷰션을 위해 컨트리뷰터 가이드 문서를 참고하는 것처럼, 2020 컨트리뷰톤 활동 결과를 어떻게 잘 정리할 수 있는지 (예: 학습한 내용을 저장하기, 커밋/이슈 등록 내용 정리) 에 대해 설명합니다. 참고로, 해당 저장소는 OpenStack 문서 컨트리뷰션을 할 때 사용하는 Restructured Text 를 기반으로 커밋을 권장하며, 오픈스택 컨트리뷰션을 위해 필요로 하는 CI/CD 관련 지식을 GitHub Pull Request에 연관지어 설명을 하고자 합니다. (아래부터 편의상 존칭을 생략합니다)
일반적으로 GitHub 등 많은 오픈 소스 저장소에서 Markdown 기반의 *.md 파일을 볼 수 있는데 반해, 많은 Python 프로젝트들은 Restructured Text (RST) 포맷을 기반으로 문서화하는 것을 선호한다 (보다 자세한 설명은 PyCon KR 2017 발표 내용 및 PEP에 등록된 링크1 및 링크2 를 참고하자). OpenStack에서 문서 컨트리뷰션은 Sphinx 라는 RST 빌더를 활용하여 RST로 작성한 커밋 내용을 HTML 및 PDF로 만들어낸다. 예를 들어, Nova 저장소를 살펴보면 api-guide/source, api-ref/source 및 doc 폴더 3개를 확인할 수 있을텐데 각각은 Compute API - Guide, Compute API - References, OpenStack Compute HTML 문서에 정확히 대응한다.
본 저장소는 OpenStack 문서 컨트리뷰션에 대한 이해 및 실제 문서 컨트리뷰션을 권장하고자 동일한 프레임워크를 사용하여 컨트리뷰톤 활동 결과를 이 저장소에 잘 정리하기 위해 어떻게 활동 내용을 커밋 형태로 본 저장소에 기록할지 설명하고자 한다.
본 저장소에 직접 쓰기를 하는 것보다 Fork를 하여 본인 GitHub 계정에 속한 저장소에서 이것저것 작업을 한 후에 풀리퀘스트를 하는 것이 GitHub에서 권장하는 방법이므로, 이 방법을 설명하고자 한다.
openstack-kr/contributhon2020 GitHub 저장소 URL로 이동한다.
오른쪽 위에 Fork 라는 버튼이 보일 것이다. 이를 클릭한 후, 본인 계정을 클릭하면 본인 계정에 Fork된 저장소가 보일 것이다 (P.S. Star 눌러주시는 센스, 아시죠? :) ).
Fork된 저장소에서 Code 버튼을 클릭하면 본인 컴퓨터 (로컬)에 클론할 수 있는 고유 URL이 보일 것이다. 이를 복사하여 로컬에 클론을 실행하면 폴더가 하나 만들어지면서 클론이 완료될 것이다.
doc/source 이 RST 문서에 대한 base 폴더가 된다. 해당 base 폴더에 다음 단계를 참고하여 RST 파일을 작성한다.
(이미 했으면 skip) 폴더를 하나 만든다. 컨트리뷰터 GitHub ID, 프로젝트 ID 등 다 괜찮다.
(이미 했으면 skip) 만든 폴더 내 index.rst 파일을 만든다. doc/source/contributors/index.rst 파일을 복사하여 참고하는 것도 좋겠다. 구체적으로는 아래 내용 및 설명을 참고하자. 설명은 # 뒤에 붙였다.
.. _contributors: # 책갈피 용도로 사용한다. _[폴더명] 정도면 괜찮을 듯 하다.
============================== # 제목 위 아래에 = 으로 표시한다. 길이가 제목과 같거나 길어야 한다.
참고: 컨트리뷰션을 위한 가이드
============================== # 위에 제목을 잘 적고, 해당 줄은 윗 부분 = 길이와 동일하게 한다.
.. toctree:: # 목차를 위한 문법 (그대로 사용하자)
:maxdepth: 1 # 1단계 목차로 끝난다는 것을 의미 (그대로 사용하자)
how_to_contribute.rst # 실제 rst 문법으로 적을 문서 파일명으로 대체
# 여러 파일로 나누고자 할 경우에는 해당 파일명도 추가하자.
(이미 했으면 skip) doc/source/index.rst 에 Line 15 근처에 위에서 만든 index 파일을 연결시켜준다. 구체적으로는 아래 내용 및 설명을 참고하자. 설명은 역시 # 뒤에 붙인다.
.. toctree:: # 최상위 문서 기준 목차 (그대로 사용하자)
:maxdepth: 2 # 2단계 목차라는 의미 (그대로 사용하자)
openstack_helm/index # 기존 컨트리뷰션된 문서 index
[만든 폴더]/index # 새로 만든 폴더 내 index를 가리키도록 추가하자.
contributors/index # 기준 컨트리뷰션된 문서 index
doc/source/[만든 폴더]/[실제 적을 rst 파일] 형태로 파일을 만들거나, 이미 만들었다면 수정하자. RST 형식을 지키면서 문서를 작성이 필요하며, 추가한 파일은 두 번째 언급한 파일에 명시해야 함을 잊지 말자.
추가하여 사용하는 이미지들은 doc/source/[만든 폴더]/images 폴더 내에 저장하도록 통일하도록 하자.
RST에 대한 Convention은 표준 방식을 따르므로 자세한 내용은 Quick reStructuredText 및 OpenStack Documentation Contributor Guide - RST Conventions 를 참고하자. 하지만 모든 것을 참고하기 쉽지 않을 수도 있기에, 지금 작성하는 문서에 대한 RST 원본 파일 을 참고한다면 많은 도움이 될 것이다. 제목은 = = 로 감싸며, 부제목은 아래만 -, 소제목은 아래에 ~ 을 사용하며 들여쓰기를 잘 해야 한다는 점, 링크 및 이미지, 코드 블럭 표시 방법을 기본으로 참고하면서 각 행은 최대 80자만 허용한다는 기본 규칙을 참고하여 작성해보자. 추가적으로 생각나는 유의 사항을 적자면 다음과 같다:
각 줄 마지막에 스페이스바를 일부러 넣지는 말자.
Sphinx 빌드 오류로 인해 문법을 사용한 다음에는 일부러 띄어쓰기를 하자. 예를 들면 다음과 같은 상황이다.
`링크 <https://example.org>`_와 같이 "_와" 사이에 띄어쓰기가 없다면 빌드가 안될 것이다.
PR (풀리퀘스트)를 하면 CI/CD를 통해 확인을 해 주기는 하나, RST 문법대로 잘 작성하여 문서가 잘 만들어지는지 로컬 환경에서 테스트를 해 보는 것이 권장사항이다. 빌드 환경은 Python 3 환경을 필요로 하며, tox 라는 프로그램을 사용한다. 보통 운영체제에 따른 Python 3 설치를 먼저 진행한 후, pip 를 Python 3용 (예: pip3)으로 설치하여 pip3 install tox 식으로 tox*를 설치 가능한데, 다양한 Python 버전 등과 함께 사용할 때는 *pyenv, pyvenv, virtualenv 등도 사용 가능하기에, 자세한 설명을 여기에서 하지는 않고자 한다. OpenStack - Building documentation 문서 및 기타 검색한 내용을 참고하면 좋겠다.
문서 빌드는 다음 명령어로 한다.
$ tox -e docs
문서 빌드가 성공적으로 된다면 doc/build/html 폴더에 있는 index.html 폴더를 열어 확인할 수 있다.
만약 문서에 rst 문법 오류가 있다면 다음과 같이 오류가 나니 참고하자.
변경 사항을 모두 추가하여 커밋을 만들자. 몇 가지 유의 및 권장 사항을 나열하고자 한다.
로컬에서 master/main 대신 별도로 브랜치를 만들어 작업하는 것을 권장하나, 브랜치에 익숙하지 않다면 편하게 작업해도 좋겠다.
관련 파일들이 모두 커밋에 추가가 되는지 확인하도록 하자. 예를 들어, 아래 스크린샷과 같은 상황에서 git commit 명령어로 커밋을 하면 rename된 결과만 커밋이 이루어지며, git commit -a 명령어로 모든 내용을 커밋하더라도 images 폴더에 있는 파일들이 커밋에 추가가 안된다.
커밋 메시지를 잘 적도록 하자. 통상 가이드되는 내용은 제목 (첫 줄)은 50자 이내로, 제목과 본문 사이에 1줄 띄우기 등이 있다. 자세한 내용은 아래 링크를 참고하자.
GitHub에서 풀리퀘스트를 하는 것은 OpenStack 컨트리뷰션에서 git review 명령어로 Gerrit Code Review 에 올리는 것과 마찬가지라고 할 수 있겠다.
만든 커밋은 로컬 컴퓨터에만 있으므로, 이를 git push 명령어를 통해 Fork한 저장소에 올려보자. (참고: 스크린샷에서는 contributor-documentation 라는 브랜치를 로컬 환경에 만들고, 해당 브랜치를 로컬 저장소에 올렸다.)
이제 본인이 fork한 저장소를 웹 브라우저로 살펴보면, 위 스크린샷과 같이 자동으로 풀리퀘스트를 제안할 것이다 (만약 자동으로 안 나오더라도 Pull Requests -> New pull request 를 통해 직접 할 수 있다).
이 때, 어떤 목적의 풀리퀘스트인지를 자세히 적어주는 것이 좋을 것이다.
참고로, OpenStack에서 사용하는 Gerrit 시스템 기반에서는 1개 커밋만 가지고 작업을 하기에, 커밋 메시지에 적는 내용이 리뷰 설명과 동일하게 간주되나, GitHub에서는 뒤에서 설명하겠지만 1개 또는 여러 개 커밋을 풀리퀘스트로 올릴 수가 있기에 별도로 풀리퀘스트 설명을 잘 적어주는 것이 중요할 것이다.
메시지를 잘 적고 올리는 것으로 풀리퀘스트 제출이 완료되는데, 3개의 확인 사항을 두었으니 풀리퀘스트 제출 전 확인해보도록 하자.
CLA 서명 여부: cla-assistant 기능을 통해, Apache License 2.0을 따른다고 본 저장소에 대해 명시를 하였으며, 라이선스 동의가 이루어져야 CI/CD를 통해 fail이 나지 않는다. 위 링크를 통해 라이선스 동의를 하도록 하자. OpenStack 역시 CLA 서명 단계가 있으며, 모두 이미 진행하였을 것이다.
이슈 연결: 본인이 진행하는 진행 상황 또는 별도로 정리한 이슈 등과 연결시키기를 권장한다. 이를 위해 GitHub: openstack-kr/contributhon2020 이슈 에 등록된 이슈 (없으면 생성)에 대한 URL을 Related Issues 섹션에 적고 체크하도록 하자. 마치 OpenStack에서 Launchpad 이나 Storyboard 에 버그/이슈를 제출하는 것과 비슷한 것이다. 실제 OpenStack에서는 커밋 메시지에 Closes-Bug #번호 식으로 적어준다.
리뷰어 내용에 대해 잘 반영할 의지가 있는지에 대한 여부이다. 동의를 하였다면 다음 섹션에서 설명하는 내용을 참고하여 잘 반영하도록 하자.
이렇게 하여 만든 커밋을 풀리퀘스트를 통해 본 저장소에 반영을 요청하는 단계까지 진행하였다. 이제부터는 리뷰어 승인을 기다려야할텐데, 그 전에 실제 커밋 및 풀리퀘스트까지 한 내용이 잘 동작하는지 잘 확인하는 단계 또한 함께 설명하고자 한다.
제출한 풀리퀘스트 URL로 들어가보자. 바로 안 나올 수도 있으나, 잠시 기다리면 먼가 자동 빌드가 이루어진다.
크게 2가지가 자동 실행되어 결과가 나온다.
license/cla: CLA 서명 여부를 확인한다.
netlify/openstack-kr-contributhon2020/deploy-preview: 풀 리퀘스트를 기준으로 빌드한 결과가 나온다.
빌드에 실패한 경우: 왜 실패하였는지 오류 메시지를 확인할 수 있다. deploy-preview 에 있는 Details 버튼을 클릭 후, 로그 메시지를 참고해보자. 어떤 것을 수정해야 하는지 힌트를 얻을 수 있을 것이다.
![]()
![]()
빌드에 성공한 경우: Preview 페이지를 통해 만든 문서가 잘 빌드되었는지 Details 를 클릭하여 확인 가능하다.
![]()
참고로, OpenStack - Gerrit에서는 커밋을 git review 명령어로 제출하면 Zuul 이라는 CI/CD에 의해 문서를 포함한 여러가지 테스트 코드가 자동으로 실행되며, 문서의 경우 openstack-tox-docs -> Docs preview site 로 이동하면 아래 스크린샷과 같이 문서 빌드가 잘 되었는지 확인이 가능하다.
CI/CD에 의해 빌드에 실패했다면 실패 이유를 참고하여 수정해야할 것이며, 성공했더라도 문서 미리보기를 통해 원하는대로 문서가 잘 빌드되었는지 확인하고, 원하는대로 되지 않았다면 역시 수정을 해야할 것이다. 또한 성공했더라도 리뷰어 코멘트에 의해 추가 반영이 필요할 수도 있겠다. 어떤 상황이 되었든 수정이 필요할텐데, 이 때 수정을 위해 크게 2가지 접근 방식이 있다. 어느 방식을 사용하든 본 컨트리뷰톤에서는 신경쓰지 않고자 하나, 오픈 소스에 따라 이 2가지 중 특정 접근 방식을 권장하기도 하는 점 또한 참고하도록 하자. 참고로 오픈스택에서는 기존 커밋을 --amend 하여 git review 명령어로 Gerrit에 올리면 git review 플러그인에 의해 자동으로 추가된 Change-Id 덕에 동일한 Gerrit Review에 패치 단위로 누적이 되어 살펴보기가 가능하다는 장점이 있겠다.
로컬 환경에서 변경을 하고 커밋을 더 만들어 git push 를 하면 된다. 이력이 100% 남는다는 것은 장점이나, 추가된 여러 개 커밋들이 풀리퀘스트에서 모두 보여 다소 지저분하다고 생각하는 리뷰어도 있는 점을 참고하자.
로컬 환경에서 변경을 하고, 기존 커밋에 대해 --amend 옵션을 통해 수정 (amend) 를 하면, 커밋이 새로 추가되는 것이 아니라 기존 커밋을 수정하는 셈이 된다. 이 커밋을 git push 명령어로 올리면 아래 스크린샷과 같이 reject 이 된다.
리젝이 되는 이유는 커밋이란 것은 기본적으로 누적이 되어야 하는데, 기존 커밋을 수정하였으니 푸시를 당하는 입장에서는 누적이 되지 않는 것이기 때문이다. 이 때 강제로 푸시하는 옵션이 --force 라고 있으며, git push --force 옵션으로 강제로 푸시를 하면 로컬에서는 강제로 푸시하였다고 나오며, GitHub 상에서는 force-pushed 하였다고는 나오나, 이전 커밋 내용이 어땠는지까지는 확인이 불가능하다는 점 참고하자.
PR이 리뷰를 거쳐 잘 승인되었다면, 승인된 커밋을 기준으로 자동으로 문서가 생성되며, 다음 URL을 통해 확인할 수 있다:
1개만 있어도 사실 상관없다고 생각할 수 있으나, 여러 이유로 3가지 어디에서든 확인이 가능하며, 혹 이에 대해 궁금하다면 이슈 내용을 살펴보자. 3rd-party 툴에 의해 동작하기에, 사정이 있어 동작하지 않을 수도 있겠으나 README.rst 에 나와있는 뱃지를 통해 위 사이트에 문제가 있는지 등 상태 확인이 가능하다.
2018년 오픈스택 컨트리뷰톤 때 역시, 비슷한 방식으로 컨트리뷰션을 권장하였으나, 당시에는 CI/CD를 이용해 문법 형식 체크까지는 하였으나 프리뷰까지는 연동은 하지 못했으며, 이슈 및 풀리퀘스트 템플릿도 존재하지 않았다. 하지만 해당 저장소에 있는 내용을 참고한다면 본인이 정리한 내용이 보다 많은 컨트리뷰터에게 유용할 수 있는지 도움이 되지 않을까 하여 참고 리소스 링크로 두고자 한다.
2018년 컨트리뷰톤 1팀: https://github.com/openstack-kr/contributhon-2018-team1
2018년 컨트리뷰톤 2팀: https://github.com/openstack-kr/contributhon-2018-team2
데브 스택을 설치하고 팀원들과 클라이언트에 대한 디버깅을 진행하는 다양한 방법을 다뤄보았습니다. 파이참과 vscode의 remote debugging을 이용한 경우가 대부분입니다. client를 원격에서 접속하는 것이 아닌, 원격의 interpreter와 client를 이용했던 이유는 크게 두 가지 정도입니다.
하지만 설정을 통해 외부 IP를 public IP로 변경할 수 있었습니다.
하지만 저의 컨트리뷰션 동안에는 주로 openstack client
에 자체에 대한 작업을 진행했기에, 이 경우가 해당되지 않았습니다.
따라서 저는 DevStack을 추가적인 몇 가지 설정과 함께 원격에서 접속할 수 있도록 설치하여
이후 이어질 PyCharm을 이용한 openstack client
디버깅 작업을 좀 더 수월하게 진행했습니다.
https://docs.openstack.org/devstack/latest/ 를 바탕으로 install하되 local.conf 에서 우리는 remote에서도 접속이 가능하도록 설정을 해줘야한다.
https://docs.openstack.org/devstack/latest/configuration.html 은 configuration에 대한 문서인데, 이 부분을 보면 다음과 같은 내용이 존재합니다.
> HOST_IP
is normally detected on the first run of stack.sh but often is indeterminate on later runs due to the IP being moved from an Ethernet interface to a bridge on the host. Setting it here also makes it available for openrc to set OS_AUTH_URL. HOST_IP is not set by default.
즉 이더넷 네트워크 인터페이스의 주 IP를 따라간다는 것인데, 원격에서도 우리의 server(devstack)으로
요청을 보내기 위해서는 cafe24 등의 public IP
로 host ip
를 변경해주어야하는 것입니다.
# local.conf
[[local|localrc]]
ADMIN_PASSWORD=secret
DATABASE_PASSWORD=$ADMIN_PASSWORD
RABBIT_PASSWORD=$ADMIN_PASSWORD
SERVICE_PASSWORD=$ADMIN_PASSWORD
HOST_IP=<183.x.x.x와 같은 public IP>
disable_service etcd3
그리고 openstack client를 통해 API를 이용하기 위한 설정을 담는 openrc``를 작성해주세요.
이렇게 ``HOST_IP``를 수동으로 설정하는 경우 ``kubernetes``의 데이터 저장소인 ``etcd
설정에서
오류가 나더라구요. 자세히 리서치해보진 못했지만 우선은 etcd를 disable하는 정도로 넘어갑니다.
$ sudo ip addr add 183.x.x.x/32 dev ens3
그리고 자신의 Public IP를 server instance 자신도 자기의 IP로 인식할 수 있도록 위의 커맨드를 입력해 ens3 interface 에 Public IP를 secondary IP로 추가해줍니다.
(이 부분의 원리는 정확히 이해하지못했습니다. 아시는 분이 계시면 알려주시기바랍니다!)
# openrc
export OS_PROJECT_DOMAIN_ID=default
export OS_USER_DOMAIN_ID=default
export OS_PROJECT_NAME=demo
export OS_TENANT_NAME=demo
export OS_USERNAME=admin
export OS_PASSWORD=secret
export OS_AUTH_URL=http://183.x.x.x/identity
export OS_IDENTITY_API_VERSION=3
위와 같이 devstack을 clone 받은 경로 안에 local.conf
를 수정해주세요.
$ ./stack.sh
clone 받은 devstack 경로 안에 local.conf
를 잘 작성해주었다면
devstack 경로 안에서 ./stack.sh
를 입력하는 것 만으로도 devstack 구축이 완료됩니다.
$ source openrc
설치가 완료된 후에는 위의 command를 통해 openrc를 적용시켜주세요.
이후 필요에 따라 보안그룹 혹은 방화벽을 수정해줘야합니다. 기본적으로 SSH는 열려있었을 것이고, 브라우저 접속을 위한 80포트, openstack client를 이용한 API 수행을 위한 TCP 6060포트와 9696포트를 추가로 열어줘야했던 것으로 기억합니다.
우선은 기본적으로 devstack이 잘 뜨는지 확인해보기위해 웹브라우저로 접속해봅니다. http://자신의PublicIP 를 통해 접속해봅시다.
기본적으로는 admin/secret을 통해 접속할 수 있으니 로그인도 해봅시다.
$ openstack user list
+----------------------------------+-----------+
| ID | Name |
+----------------------------------+-----------+
| 40ee1fb2103c40d5b077a98a0318d225 | admin |
| 5cd3c4104d214e0992f671ae7408a001 | demo |
| 5b4ee20cae26492db5809f2fd8f15659 | alt_demo |
| 02d8ea737a3248bb94a9a61fe53d0921 | nova |
| be0b5fa743ab4a588708515c5c5c7645 | glance |
| cf795d9a36804446b88a4d8b5fccf41c | cinder |
| 3bf64dd3cc0c4f20ab78cc10d239d242 | neutron |
| ea2ead4f752a4b668421fada1cadb78a | placement |
+----------------------------------+-----------+
$ git clone https://github.com/openstack/python-openstackclient
python-openstackclient
를 clone 받아준 뒤에 PyCharm으로 열어줍니다.
위와 같이 openrc 에 있던 데이터들 중 export를 제외해서 복사한 뒤 ADD CONFIGURATION
에서
Python interpreter를 설정해줍니다. Script Path는 openstackclient/shell.py의 절대경로를 입력해주세요.
그럼 위의 사진들과 같이 remote debugging 없이도, openstack client 자체가 remote openstack server로 요청을 보내기 때문에 Default 혹은 그 외의 Local interpreter로도 디버깅 작업이 가능해집니다.
이 글의 주요한 내용은 아니지만, 먼저 vscode와 pycharm을 통해 remote debugging 관련
내용을 정리해주신 팀원들께 정말 감사했고, 솔선수범하여 문서를 정리해주고, 알려주시던 모습들이
기억에 남습니다.
그리고 거기서 더 나아가 '원격에서 접속이 불가능했다'는 근본적인 불편 사항을 해결하기 위해
흔쾌히 도움을 주신 멘토님께 감사합니다.
당시 그 원리는 자세히 몰랐으나, 멘토님이 제시해 주신 해결 방안에 네트워크와 관련된 내용이
포함되어있었는데, 문득 다시 한 번 기본기와 네트워크의 중요성을 느낄 수 있었던 기회였던 것 같습니다.
만약 이 방법을 이용하지 않고, 단순히 remote debugging 만을 이용했다면 virtual env를 이용하거나
openstack client
를 재설치하거나 git을 이용하는 등의 다양한 작업에서
제약이 따랐을 것 같은데, 이 방법으로 DevStack을 설치한 덕에 편리하게 작업을 수행할 수 있었습니다.
gerrit review 링크: https://review.opendev.org/#/c/746369/
openstack client를 통해 server show 명령을 수행하는 경우 기본적으로는
output format이 table
이고 그 경우 위와 같이 output이 줄바꿈형식으로 깔끔하게
출력됩니다.
하지만 문제는 위와 같이 table format이 아닌 json` 이나 yaml` 인 경우에도 단순히 줄바꿈과 쉼표를 이용해 출력이 된다는 것이었습니다.
보통은 json
이나 yaml
형태로 output을 얻는 경우는 이후에 그 output을 이용해
어떠한 프로그래밍적 로직을 수행하려하는 경우일텐데, 단순 줄바꿈과 쉼표를 이용하는 경우
output이 출력되는 경우에는 다시 output을 파싱해야하는 번거로움이 존재할 것입니다.
따라서 위의 사진처럼 output format이 json
, yaml
인 경우에는 기존의 방식이 아닌, dictionary
나 object
, array
형태로 output 출력하도록 패치했습니다.
앞서 보여드린 사진과 같이 다음의 명령어를 수행할 경우, json, yaml에서도 단순히 줄바꿈과 쉼표를 통해 output을 출력하는 것을 보실 수 있습니다. (가독성을 위해 security_groups column만 이용해보겠습니다.)
openstack server show umi0410 -c security_groups -f json
{
"security_groups": "name='default'\nname='tmp'"
}
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
server = utils.find_resource(compute_client.servers,
parsed_args.server)
if parsed_args.diagnostics:
(resp, data) = server.diagnostics()
if not resp.status_code == 200:
self.app.stderr.write(_(
"Error retrieving diagnostics data\n"
))
return ({}, {})
else:
data = _prep_server_detail(compute_client,
self.app.client_manager.image, server,
refresh=False)
return zip(*sorted(data.items()))
openstackclient/compute/v2/server.py
의 take_action 함수에서
API를 수행하고, result를 return하는 과정이 정의되어있습니다.
이 때 _prep_server_detail
이라는 함수가 server
객체의 정보를 담은
dictionary를 return 해줍니다.
하지만 문제는 dictionary로 변경하는 과정에서 줄바꿈형식으로 table처럼 formatting 하는 로직이 들어있었고, output format이 table이 아닌 경우는 이 부분이 불필요했습니다.
# 변경한 코드
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
server = utils.find_resource(compute_client.servers,
parsed_args.server)
formatter_type = parsed_args.formatter
if parsed_args.diagnostics:
(resp, data) = server.diagnostics()
if not resp.status_code == 200:
self.app.stderr.write(_(
"Error retrieving diagnostics data\n"
))
return ({}, {})
elif formatter_type == "table":
data = _prep_server_detail(compute_client,
self.app.client_manager.image, server,
refresh=False)
else:
data = server.to_dict()
data.update(
{"properties": data.pop("metadata"),
"volumes_attached":
data.pop("os-extended-volumes:volumes_attached")}
)
return zip(*sorted(data.items()))
따라서 result로서 사용되는 dictionary를 만드는 로직을 table인 경우와 그 외의 경우로 나눠서 진행하도록 하였습니다.
기존의 test code를 까보고 꽤나 놀라웠습니다. test code에서는 앞서 말씀드린 예상된 불편함이었던 output을 다시 파싱해서 테스트를 통과하는지를 판단하고있었습니다.
self.assertTrue(volumes_attached.startswith('id='))
attached_volume_id = volumes_attached.replace('id=', '')
이런 식으로 dictionary
로서 ["id"]
의 값을 확인하는 것이 아니라
단순 문자열 id=의 형태로 시작하는지를 판단하는 test도 있었고,
# Really, shouldn't this be a dict?
self.assertEqual(
"a='b', c='d'",
cmd_output['properties'],
)
이렇게 테스트 코드 내의 주석에서도 불편을 호소하는 경우가 있었습니다.
데브 스택을 설치하고 팀원들과 클라이언트에 대한 디버깅을 진행하는 다양한 방법을 다뤄보았습니다.
self.assertEqual(
{'a': 'b', 'c': 'd'},
cmd_output['properties'],
)
따라서 저는 위와 같이 dictionary로서 접근할 수 있도록하였고, Zuul에서 모든 test를 통과하도록 수정하였습니다.
Fix miss formatting array to print in ShowServer
Currently, When the formatter is not table and result of content contains list, the items of list were printed as a concatenate with comma.
In this patch, if the output format is not table, the output of list will follow the output format. (e.g. json output = [ ]) This will put right output format for not only table but also both json and yaml.
Change-Id: Ibf88593b1935c7ff42a5512136e0eca9f8466343 Story: 2007755
수정내역을 반영해 Gerrit에 review를 요청했고, https://review.opendev.org/#/c/746369/ 에서 확인해볼 수 있습니다.
누군가가 올린 흥미로운 이슈를 찾는 것, 재현하는 것, 픽스하는 것은 재미있는 경험이었습니다.
그것을 위해 storyboard
, launchpad
등에 가입하고, tox
를 통해 test를 돌려보고,
Zuul
을 통해 다시 자동화테스트를 거쳐 gerrit
을 통해 review를 받는 일련의 과정들이
혼자였다면 어렵게만 느껴졌을텐데, 친절한 멘토님의 도움과 적극적인 팀원들의 참여가 있었기에
수월히 해낼 수 있었던 것 같아 감사했고, 재미있었습니다.
During a recent infrastructure test exercise we completely pulled the plug on a whole AZ. To our surprise, openstack availability zone list reported all AZs as available even if one of them was clearly down. This is confusing for tools that check on that output to detect full AZ failures.
Issue: https://storyboard.openstack.org/#!/story/2006816
Availability zone
을 수동으로 비활성화시켰음에도, $ openstack availability zone list
에서는 AZ가 Healthy한 state로 나온다는
Issue가 있었습니다. 이 Issue를 제 환경에서 실행해보았고, 저는 bug가 재현되지 않았기에,
저의 내용을 추가적으로 Comment에 기입해주었습니다.
우선 Aggregate란.
Host aggregates provide a mechanism to group hosts according to certain criteria.
aggregate를 만들어주고, 거기에 host를 추가함으로써 availability zone을 만들 수 있고, host를 disable 함으로써 availability zone이 not available 하게 만드는 흐름입니다.
$ openstack aggregate create dev-aggregate
$ openstack aggregate set --zone dev-zone dev-aggregate
$ openstack aggregate add host dev-aggregate openstack-master
$ openstack availability zone list
+-----------+-------------+
| Zone Name | Zone Status |
+-----------+-------------+
| dev-zone | available |
| internal | available |
| nova | available |
| nova | available |
| nova | available |
+-----------+-------------+
$ nova service-list
$ nova service-disable <service_id>
$ openstack availability zone list
+-----------+---------------+
| Zone Name | Zone Status |
+-----------+---------------+
| internal | available |
| dev-zone | not available |
| nova | available |
| nova | available |
| nova | available |
+-----------+---------------+
아마도 Nova가 자신이 API를 수행하면서는 AZ의 availability를 체크를 하지만, random unhealthy state에 대한 반영은 하지 않을 수도 있겠다는 생각을 했습니다.
따라서 혹시 저와 같이 수행했을 때에도 Bug가 재현되는지, Issue 제기자의 좀 더 자세한 상황이 어땠는지를 여쭤보는 Comment를 달아 다른 사람들과 소통해보고자 했습니다.
사실 버그가 재현되어서 하나 버그 픽스를 해보고싶었지만, 저의 방법에서는 재현되지 않아서 조금 아쉬웠습니다. 또한 Availability Zone이라는 것이 개인적인 실습 수준에서는 쉽게 접근하기 어려운 내용이라 어려웠던 점이 있었던 것 같습니다.
하지만 꼭 버그 픽스나 merge가 아니더라도 이런 식으로 소소하게 기여를 하다보면, 점점 큰 기여를 할 수 있는 밑거름이 될 것이라 생각합니다!
launchpad id : bo314
zanata id : umi0410
stackanalytics pr : https://github.com/stackalytics/default_data/pull/209
이 글은 샌드박스 튜토리얼 문서 과 2020 컨트리뷰톤 오프라인 모임에서 실습한 것을 토대로 작성되었습니다.
오픈스택은 gerrit이라는 코드 협업 도구를 사용합니다. gerrit으로 만들어진 사이트가 바로 Review opendev 입니다.
openstack만의 workflow를 따르는데, 우리가 알고 있는 깃허브의 PullRequest(PR) 과는 다른 방식으로 운영됩니다.
그림으로 간단하게 살펴보겠습니다.
사용자가 nova/master를 자신의 개발환경(로컬)에 clone을 합니다.
열심히 브런치를 파고 코드를 수정합니다.
수정을 하고 단위 테스트를 하고 잘되면 커밋을 합니다.
git review 명령어를 이용해 커밋 규칙 에 잘 맞게 Gerrit 올립니다.
높은 리뷰 점수를 받게 되면 지속적 통합(CI) 도구인 Zuul을 사용해 코드가 병합됩니다.
물론 이건 한번에 성공했을 때 사례고 실제로는 한 번에 성공하기는 쉽지 않을 겁니다. 아마 누군가에게 피드백을 받게 될 것입니다.
그럴 경우 수정해서 올려야 되는데, github에서는 commit을 쌓아서 PR을 보낸다고 했을 때 (물론 git flow 따라 다르겠지만..) 오픈스택의 WorkFlow는 커밋을 쌓지 않고 하나의 커밋을 수정해서 리뷰사이트에 올립니다.
그러기 위해서는 git commit --amend로 수정해서 올려야 합니다. 이제 본격적으로 샌드박스로 튜토리얼 실습을 진행해 보겠습니다.
Gerrit 및 ubuntuOne등 필요한 회원가입 및 로그인이 되어 있다는 것을 전제로 진행하겠습니다.
첫 번째로 오픈스택 런치패드에서 버그를 생성해서 올려보겠습니다.
오픈스택 런치패드 는 버그를 올리고, 코드를 관리하는 용도로 사용됩니다. 프로젝트마다 차이는 있지만, 최근에는 스토리보드 로 옮겨가는 추세라고 합니다.
Projetcs 제목 아래로 스크롤을 해보면 여러 프로젝트들을 살펴볼 수 있는데, 테스트를 할 수 있는 openstack-dev-sandbox 로 이동해 보겠습니다.
Bug를 누르면 다양한 버그목록들을 볼 수 있습니다.
이 버그들은 연습용 버그 목록들입니다. 여기에 버그를 리포팅을 하겠습니다.
Report a bug 버튼을 눌러줍니다.
간단하게 버그에 대해 요약을 적어줍니다.
새로운 버그에 대해 정보를 쓴 뒤 등록해줍시다.
버그가 등록되었습니다.
Assinged to로 버그를 할당하여, 다른 사람들에게 알릴 수 있습니다. 스스로에게 이 버그를 할당하였습니다.
버그 수정을 하기 위해선 오픈스택에서 사용하는 리뷰 시스템인 Gerrit 에서 회원 가입 및 일부 설정을 해주어야합니다.
Gerrit에 대한 자세한 설명은 여기 에서 볼 수 있습니다.
sign in을 눌러 회원가입 및 로그인은 각자 진행해줍니다. ubuntuOne계정이 미리 있어야 합니다. (추가로 Gerrit의 username은 입력 후 변경할 수 없으며, 깃허브 아이디를 입력할 경우 연동이 된다고 하는데 아직 제대로 된 활동을 해보지 않아서 확실하지는 않습니다. 추후 수정하겠습니다.)
로그인이 되었으면 우측 상단의 계정을 누른 뒤 Settings를 눌러줍니다.
New Contributor Agreement를 눌러 오픈스택 동의서를 제출해야 합니다. 저는 개인이라 ICLA를 선택하여 동의 후 제출을 했습니다.
SSH Public Key(공개키)를 등록해 주어야 합니다. 공개키를 등록하기 위해서는 private key(비밀키)를 생성을 해주어야 합니다.
이후 실습 및 키 생성은 ubuntu18.04 LTS 환경에서 진행하겠습니다.
ssh-keygen
cat ~/.ssh/id_rsa.pub
ssh-keygen을 이용하면 공개키(id_rsa.pub)와 비밀키(id_rsa)가 생기는데, 공개키인 id_rsa.pub가 필요합니다. (절대 비밀키는 노출되면 안 됩니다.)
공개키를 복사한 뒤 Add를 눌러 추가시켜주면 끝납니다.
이것으로 gerrit에서의 설정은 끝입니다.
git clone https://opendev.org/opendev/sandbox.git
cd sandbox
먼저 git 저장소 에서 다운을 받아 줍시다.
git remote -v
현재 origin을 보면 우리가 clone 한 저장소인 것을 알 수 있습니다.
우리는 gerrit으로 올려야 하므로 git config 설정을 해주어야 합니다.
git config --local user.name "name"
git config --local user.email "email@gmail.com"
git config --global gitreview.username "gerrit username"
공식 문서에서 gitreview.username은 Gerrit에서 설정한 Username을 넣어줘야 합니다.
sudo apt install git-review
git-review를 설치해줍시다. 설치방법은 여기 에 가시면 환경별 설치방법을 보실 수 있습니다
git review -s
git remote -v
git review가 우리가 gerrit에 등록한 RSA 키를 사용해 remote정보를 등록해줍니다.
이제 원격지가 origin뿐만 아니라 gerrit도 추가된 것을 볼 수 있습니다.
s 옵션은 다음과 같습니다.
위에서 우리가 만든 버그를 해결하겠습니다. 버그 수정을 한다는 가정으로 소스코드를 수정해보겠습니다.
bug.txt라는 파일을 생성한 다음, bug fix라는 내용을 적었습니다.
git add bug.txt
git commit
위와 같이 commit 메시지를 작성하였습니다. 제목과 내용, 그리고 Closes-Bug와 #1891703을 작성해주었습니다.
실제 컨트리뷰트를 할 때는 오픈스택 커밋 규칙 이 있기 때문에 이에 맞게 해 주셔야 합니다.
커밋에 내용에 대한 설명은 아래에 설명해 놓았습니다.
Closes-Bug의 #1891703의 정체는 바로 런치패드에 있는 URL의 맨 뒤에 있는 고유번호입니다.
자신이 만든 버그 리포트 페이지에서 각자 생성된 고유번호를 쓰시면 됩니다.
런치패드(Launchpad)에서는 Closes-Bug, Partial-Bug 태그가 있고, 스토리보드(StoryBoard)는 Task, Story 태그를 사용할 수 있는 것을 볼 수 있습니다.
git review
이와 비슷하게 뜬다면 정상적으로 Gerrit에 코드가 올라갔다고 볼 수 있습니다. New Changes에 나와 있는 주소로 접속을 해보겠습니다.
링크: review.opendev.org/746377
Code 리뷰를 할 수 있는 사이트인 Gerrit에 내가 커밋한 코드가 정상적으로 올라온 것을 확인할 수 있습니다.
이렇게 올리는 단위를 Patch Set라고 부릅니다. 우리가 코드를 올릴 때마다 Patch Set이 추가됩니다. (1, 2, 3...)
또한 메일도 날아옵니다.
이제 수정한 코드를 저장소로 Merge 해보도록 하겠습니다.
우리가 올린 코드를 maintainer분들께서 리뷰를 해주시고, -2점 ~ 2점 사이의 점수를 줄 수 있습니다. 최소한 2점 1개를 받아야 Merge 할 수 있습니다.
제가 maintainer라고 생각을 하고 2점을 주었습니다. 핳
점수에 대한 자세한 설명은 여기 에서 볼 수 있습니다.
이렇게 2점을 받고 난 뒤 Workflow+1 버튼을 누르게 되면...
지속적 통합(CI) 도구인 Zuul이 돌게 되면서 체크를 하게 되고,
Merged 된 것을 볼 수 있습니다.
이제 Merge까지 완료가 되었으니, 직접 확인해 봐야겠죠?
먼저 우리가 git clone 받았던 저장소로 가보겠습니다.
링크: https://opendev.org/opendev/sandbox.git
Merge 됐습니다.
런치패드도 한번 가보겠습니다.
링크: https://bugs.launchpad.net/openstack-dev-sandbox/+bug/1891703
Fix Released 되었습니다.
친절하게 OpenStack infra가 commit 되었다고 저장소 링크까지 달아주었습니다.
참고 사이트
오픈스택의 한국어 팀은 korea I18Team 에서 확인할 수 있습니다.
그리고 번역 가이드 문서는 I18n 번역 가이드 문서 - 한국어 에서 확인할 수 있습니다. 번역 가이드 문서에는 번역 인프라 , 역할 , 번역 버그 다루기 등 다양한 내용들을 담고 있습니다.
오픈스택에서는 Zanata를 이용해 번역에 손쉽게 기여할 수 있습니다.
먼저 오픈스택 zanata 대시보드 페이지 로 접속을 한 뒤, Log in을 눌러줍니다.
Log in을 누르면 오픈스택ID 페이지로 접속이 되는데, 없다면 회원가입을 한 뒤, 로그인을 해줍시다.
Languages를 눌러 줍니다.
ko를 검색한뒤 ko-KR을 선택해 줍니다.
한국어 번역을 하시는 멤버분들을 볼 수 있습니다. 현재 48명이 계시는군요.
Request To Join을 눌러 한국팀에 합류를 해보겠습니다.
열심히 자기소개서를 작성한 뒤에 요청을 보내줍니다.
만약 요청을 보내고 1주일 정도 지났는데 연락이 없다면, 한국 IRC 채널 이나, 페이스북 에 도움을 요청해보는 것도 좋은 방법인거 같습니다. 저는 컨트리뷰톤 멘토님께 직접 말씀 드렸더니 승인되었습니다.
승인이 되었으니 이제 번역을 해보겠습니다.
Explore를 누르면 프로젝트들을 볼 수 있습니다.
contributor-guide 프로젝트를 번역해보겠습니다.
링크: translate.openstack.org/project/view/contributor-guide
master를 선택해 줍니다.
Languages에 korea를 검색하면 번역 상태들을 볼 수 있습니다.
Zanata에는 남은 번역 비율에 비례하여 퍼센티지를 볼 수 있습니다. 현재 2.59%가 번역이 되었네요.
doc-contributing을 번역해보겠습니다.
이제 번역하고 싶은 단어를 클릭해서 번역하면 됩니다.
단어집 을 펴놓고 하는것도 도움이 됩니다.
예를 들어 contribution, contributor 는 컨트리뷰션과 컨트리뷰터로 번역 되어야합니다.
링크: https://wiki.openstack.org/wiki/단어집
Incomplete는 체크., Fuzzy와 Rejected에는 체크를 해제해야 한다고 합니다.
Fuzzy는 자나타에서 자동으로 번역을 넣어주는 역할을 하는데, 번역을 하다 컨트롤 엔터를하면 Fuzzy와 Rejected가 자동으로 저장이 될수 도 있는 상황이 발생한다고 합니다.
한국어 번역팀: https://wiki.openstack.org/wiki/I18nTeam/team/ko_KR
스토리 보드: https://storyboard.openstack.org/#!/story/2007777
스토리 보드의 내용은 다음과 같습니다.
이미지 업로드시 glance client
의 --progress
옵션을 openstack client
에서도
지원을 했으면 좋겠다는 이슈입니다.
아마 glance client
에 있는 progress 기능을 openstack client
로 이식하면 될 거 같습니다.
재현을 위해 기존 devstack 서버에 glance client
설치를 하였습니다.
apt install glance #패키지 관리자를 이용해 glance 를 설치
source openrc admin # devstack 폴더에서 접속 계정을 admin로 설정
glance client
가 제대로 설치가 되었는지, 그리고 openstack client
에서 이와 비슷하게 사용되는
사용되는 커맨드는 무엇인지 찾기 위해 image list 를 조회하는 커맨드를 입력해보았습니다.
stack@server1:~/devstack$ glance image-list
+--------------------------------------+--------------------------+
| ID | Name |
+--------------------------------------+--------------------------+
| aa08f22f-d505-4aa3-80e4-49f34cae21e2 | 2u2buntu5 |
| 3b6445d7-a301-40fa-82ca-205b341bf41e | cirros |
+--------------------------------------+--------------------------+
stack@server1:~/devstack$ openstack image list
+--------------------------------------+--------------------------+--------+
| ID | Name | Status |
+--------------------------------------+--------------------------+--------+
| aa08f22f-d505-4aa3-80e4-49f34cae21e2 | 2u2buntu5 | active |
| f5b65c06-d5aa-47b4-b304-ef95b3d31a9d | cirros | active |
+--------------------------------------+--------------------------+--------+
openstack clinet
에서는 Status 를 컬럼을 추가로 보여주는 것을 제외하고는 같은 결과 값을 내보내는 것을 확인할 수 있었습니다.
같은 서버로 요청을 받아서 처리하는 것을 확인했으니, glance에 progress 옵션을 주어 이미지를 업로드 해보겠습니다.
#서버에 업로드 하기 위한 이미지 다운
stack@server1:~$ wget http://cloud-images-archive.ubuntu.com/releases/bionic/release-20200519.1/ubuntu-18.04-server-cloudimg-arm64.img
#glance client를 사용해 다운 받은 이미지 업로드
stack@server1:~$ glance image-create --name "ubuntu" --file /opt/stack/ubuntu-18.04-server-cloudimg-arm64.img --disk-format qcow2 --container-format bare --visibility public --progress
[=============================>] 100%
+------------------+----------------------------------------------------------------------------------+
| Property | Value |
+------------------+----------------------------------------------------------------------------------+
| checksum | 894d3c7009fc7ce476fdf2fabd403745 |
| container_format | bare |
| created_at | 2020-09-01T20:48:49Z |
| disk_format | qcow2 |
| id | f78f5ce8-b6cc-4998-b4d5-ed6f71d11f7e |
| min_disk | 0 |
| min_ram | 0 |
| name | ubuntu |
| os_hash_algo | sha512 |
| os_hash_value | 4683e1da762e246c7e2ddaecb4830af1a31da0bd2b25c1554ed288e24b79a14c6e42cb5b4a32146c |
| | fc0e7b61b64e3de4585be8fb0f4c4891ddba5290505d7c4a |
| os_hidden | False |
| owner | c4c153328c74498cba350e0db85c3a67 |
| protected | False |
| size | 327352320 |
| status | active |
| tags | [] |
| updated_at | 2020-09-01T20:48:52Z |
| virtual_size | Not available |
| visibility | public |
+------------------+----------------------------------------------------------------------------------+
glance를 사용해서 업로드를 한 결과 [=============================>] 100%
같은 형태로 진행률과 함께 업로드 되는 것을 볼 수 있습니다.
glance client는 python-glanceclient 에 코드가 있습니다. glance client 에서 progress 옵션을 어디에서 주는지 찾아보았습니다.
전체 검색으로 --progress
를 검색해 봤습니다.
@utils.arg('--progress', action='store_true', default=False,
help=_('Show upload progress bar.'))
@utils.arg('id', metavar='<IMAGE_ID>',
help=_('ID of image to upload data to.'))
@utils.arg('--store', metavar='<STORE>',
default=utils.env('OS_IMAGE_STORE', default=None),
help='Backend store to upload image to.')
def do_image_upload(gc, args):
"""Upload data for a specific image."""
# 생략
glance client에서는 위와 같은 방법으로 옵션 인자(arguments)를 넣어 주는 듯 합니다.
그리고 밑으로 좀 내리면 def do_image_upload()
, def do_image_create_via_import()
, def do_image_create()
등 여러 함수들을 볼 수 있습니다.
이 중에 업로드 기능을 구현하고자 하므로, do_image_upload
함수를 살펴보겠습니다.
링크: do_image_upload 함수 stable/ussuri 버전
def do_image_upload(gc, args):
"""Upload data for a specific image."""
backend = None
if args.store:
backend = args.store
# determine if backend is valid
_validate_backend(backend, gc)
image_data = utils.get_data_file(args)
if args.progress:
filesize = utils.get_file_size(image_data)
if filesize is not None:
# NOTE(kragniz): do not show a progress bar if the size of the
# input is unknown (most likely a piped input)
image_data = progressbar.VerboseFileWrapper(image_data, filesize)
gc.images.upload(args.id, image_data, args.size, backend=backend)
do_image_upload
함수는 python-glanceclient/glanceclient/v2/shell.py
에 위치하고 있습니다.
여기에서 주목해야 할 부분은 if args.progress:
입니다.
우리가 --progress
옵션을 주었을 때만 해당 if 문이 참(True)
이 되게 됩니다.
filesize를 구하고, None이 아닐 경우에만 image_data = progressbar.VerboseFileWrapper(image_data, filesize)
함수가 작동 되네요.
progressbar.VerboseFileWrapper()
이 친구에게 인자값을 넘겨주고, 받은 데이터를 그대로 사용하는 것을 볼 수 있습니다. 직역하면 자세한 파일 감싸기(래퍼) 라는 이름을 가지고 있습니다.
저 친구를 어디에서 데리고 왔는지 progressbar.
가 선언된 곳 을 찾아보겠습니다.
import json
import os
import sys
from oslo_utils import strutils
from glanceclient._i18n import _
from glanceclient.common import progressbar
제일 최상단에서 import 하고 있었습니다. from glanceclient.common 에서 import 했으므로 해당 코드 로 이동해보겠습니다.
class VerboseFileWrapper(_ProgressBarBase):
"""A file wrapper with a progress bar.
The file wrapper shows and advances a progress bar whenever the
wrapped file's read method is called.
"""
def read(self, *args, **kwargs):
data = self._wrapped.read(*args, **kwargs)
if data:
self._display_progress_bar(len(data))
else:
if self._show_progress:
# Break to a new line from the progress bar for incoming
# output.
sys.stdout.write('\n')
return data
_ProgressBarBase
를 상속받은 VerboseFileWrapper
클래스를 사용했네요.
openstack-clinet
에 python-glanceclient/glanceclient/common/progressbar.py
를 이식하면 progressbar를 사용할 수 있을거 같습니다.
이제 코드 이식을 위해 openstack-client 로 가보겠습니다.
class CreateImage(command.ShowOne):
_description = _("Create/upload an image")
deadopts = ('size', 'location', 'copy-from', 'checksum', 'store')
def get_parser(self, prog_name):
parser = super(CreateImage, self).get_parser(prog_name)
# TODO(bunting): There are additional arguments that v1 supported
# that v2 either doesn't support or supports weirdly.
# --checksum - could be faked clientside perhaps?
# --location - maybe location add?
# --size - passing image size is actually broken in python-glanceclient
# --copy-from - does not exist in v2
# --store - does not exits in v2
parser.add_argument(
"name",
metavar="<image-name>",
help=_("New image name"),
)
# 생략
openstack-clinet 에서 이미지는
class createImage
클래스의 def get_parser()
에서 인자(arguments)를 추가 시켜줄 수 있고, def take_action()
에서 실제 이미지 업로드가 됩니다.
def get_parser()
에서 --progress 옵션을 추가시켜 보겠습니다.
def get_parser(self, prog_name):
#생략
public_group.add_argument(
"--shared",
action="store_true",
help=_("Image can be shared"),
)
parser.add_argument(
"--progress",
action="store_true",
default=False,
help=_("Show upload progress bar."),
)
--progress
옵션을 추가 시켰습니다.
def take_action(self, parsed_args):
# open the file first to ensure any failures are handled before the
# image is created. Get the file name (if it is file, and not stdin)
# for easier further handling.
(fp, fname) = get_data_file(parsed_args)
info = {}
if fp is not None and parsed_args.volume:
raise exceptions.CommandError(_("Uploading data and using "
"container are not allowed at "
"the same time"))
if fp is None and parsed_args.file:
LOG.warning(_("Failed to get an image file."))
return {}, {}
elif fname:
kwargs['filename'] = fname
elif fp:
kwargs['validate_checksum'] = False
kwargs['data'] = fp
# 생략
if parsed_args.volume:
#생략
else:
image = image_client.create_image(**kwargs)
(fp, fname) = get_data_file(parsed_args) 에서 파일 포인터와 파일 이름을 가져오는 것을 확인 할 수 있습니다.
이렇게 가져온 데이터를 kwargs['data']에 넣는데, 이렇게 넣은 kwargs['data']는 image = image_client.create_image(**kwargs) 에 전달 되면서 서버에 이미지 data가 업로드 되게 됩니다.
fp 를 VerboseFileWrapper로 감싸면 progressbar 를 구현할 수 있을것이라고 생각하고 접근하였습니다.
이를 위해 python-openstackclient/openstackclient/common/
위치에 python-glanceclient/glanceclient/common/progressbar.py
를 복사한
progressbar.py
를 만들었습니다.
glance-client의 progressbar.py 를 그대로 옮겨왔다고 생각하시면 됩니다.
from openstackclient.common import progressbar
#생략
def take_action(self, parsed_args):
# open the file first to ensure any failures are handled before the
# image is created. Get the file name (if it is file, and not stdin)
# for easier further handling.
(fp, fname) = get_data_file(parsed_args)
info = {}
if fp is not None and parsed_args.volume:
raise exceptions.CommandError(_("Uploading data and using "
"container are not allowed at "
"the same time"))
if fp is None and parsed_args.file:
LOG.warning(_("Failed to get an image file."))
return {}, {}
if fp is not None and parsed_args.progress:
filesize = os.path.getsize(fname)
if filesize is not None:
kwargs['validate_checksum'] = False
kwargs['data'] = progressbar.VerboseFileWrapper(fp, filesize)
if parsed_args.progress:
가 나올 경우, VerboseFileWrapper 를 사용해 파일 포인터를 감싸서 kwargs['data']에 넣어 주었습니다.
stack@server1:~$ openstack image create ubuntu123 --file /opt/stack/ubuntu-18.04-server-cloudimg-arm64.img --disk-format qcow2 --container-format bare --public --progress
base_proxy
[=============================>] 100%
+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Field | Value |
+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| container_format | bare |
| created_at | 2020-09-02T12:50:17Z |
| disk_format | qcow2 |
| file | /v2/images/5f70aa73-7f14-4244-8696-383fabb1d30a/file |
| id | 5f70aa73-7f14-4244-8696-383fabb1d30a |
| min_disk | 0 |
| min_ram | 0 |
| name | ubuntu123 |
| owner | c4c153328c74498cba350e0db85c3a67 |
| properties | os_hidden='False', owner_specified.openstack.md5='', owner_specified.openstack.object='images/ubuntu123', owner_specified.openstack.sha256='', self='/v2/images/5f70aa73-7f14-4244-8696-383fabb1d30a' |
| protected | False |
| schema | /v2/schemas/image |
| status | queued |
| tags | |
| updated_at | 2020-09-02T12:50:17Z |
| visibility | public |
+------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
이미지를 업로드 할 시 openstack-client 에서도 progressbar([=============================>] 100%) 가 보이는 것을 확인할 수 있습니다.
테스트 코드를 작성해보기 전에!!
먼저 기존 테스트 코드를 돌려보는 연습부터 해보겠습니다.
오픈스택 wiki test 문서 를 보면 테스트 하는 방법이 나와있습니다.
openstack 테스트코드를 Tox 사용해 테스트하기 글 을 참고해주세요.
먼저 기존 glance client의 test 케이스 를 살펴보았습다.
python-glanceclient/glanceclient/tests/unit/test_progressbar.py
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import io
import sys
import requests
import testtools
from glanceclient.common import progressbar
from glanceclient.common import utils
from glanceclient.tests import utils as test_utils
class TestProgressBarWrapper(testtools.TestCase):
def test_iter_iterator_display_progress_bar(self):
size = 100
# create fake response object to return request-id with iterator
resp = requests.Response()
resp.headers['x-openstack-request-id'] = 'req-1234'
iterator_with_len = utils.IterableWithLength(iter('X' * 100), size)
requestid_proxy = utils.RequestIdProxy((iterator_with_len, resp))
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeTTYStdout()
# Consume iterator.
data = list(progressbar.VerboseIteratorWrapper(requestid_proxy,
size))
self.assertEqual(['X'] * 100, data)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
def test_iter_file_display_progress_bar(self):
size = 98304
file_obj = io.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
def test_iter_file_no_tty(self):
size = 98304
file_obj = io.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeNoTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
# If stdout is not a tty progress bar should do nothing.
self.assertEqual('', output.getvalue())
finally:
sys.stdout = saved_stdout
test_iter_iterator_display_progress_bar는 VerboseIteratorWrapper를 테스트하는 것으로 보입니다.
test_iter_file_display_progress_bar와 test_iter_file_no_tty는 VerboseFileWrapper를 테스트하는 것으로 보입니다.
또한 두 개의 차이점은 FakeTTYStdout()를 사용하냐, FakeNoTTYStdout()를 사용하냐에 따라 달라지는 것으로 보입니다.
대략 훑어 보았으니, 테스트 코드를 실행해보겠습니다.
tox -e py unit.test_progressbar.TestProgressBarWrapper
# 길어서 생략
==============
- Worker 0 (1 tests) => 0:00:00.000509
- Worker 1 (1 tests) => 0:00:00.000577
- Worker 2 (1 tests) => 0:00:00.000595
Test id Runtime (s)
------------------------------------------------------------------------------------------------------- -----------
glanceclient.tests.unit.test_progressbar.TestProgressBarWrapper.test_iter_iterator_display_progress_bar 0.001
glanceclient.tests.unit.test_progressbar.TestProgressBarWrapper.test_iter_file_display_progress_bar 0.001
glanceclient.tests.unit.test_progressbar.TestProgressBarWrapper.test_iter_file_no_tty 0.001
________________________________________________ summary ________________________________________________
py: commands succeeded
congratulations :)
unit 폴더의 test_progressbar.py 파일에 TestPrgressBarWrapper를 실행시켜봤습니다. 3개의 메소드에 대한 테스트가 성공한 것을 볼 수 있습니다.
이제 test_iter_file_display_progress_bar 메소드를 간단하게 분석해보겠습니다.
def test_iter_file_display_progress_bar(self):
size = 98304
file_obj = io.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
x로 채워진 파일을 만들어 낸 다음, 해당 파일을 stdoutVerboseFileWrapper()을 사용해 file_obj를 리턴 받습니다.
chunksize 만큼 파일을 읽기 시작하는데, 읽을 파일이 없을 때 까지 while문을 돌게 됩니다. 파일을 끝까지 다 읽게 됩니다.
최종적으로 output.getvalue()의 값과 [%s>] 100%%n' % ('=' * 29)의 값을 비교하면서 하면서 끝납니다.
정말 파일을 다 읽는지 궁금하여, loging 을 import 하여 출력해봤습니다.
import logging
#생략
def test_iter_file_display_progress_bar(self):
size = 30
logging.warning("test_iter_file_display_progress_bar")
file_obj = io.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = test_utils.FakeTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 14
chunk = file_obj.read(chunksize)
logging.warning('chunk: ' + chunk)
while chunk:
chunk = file_obj.read(chunksize)
logging.warning('chunk: ' + chunk)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
logging.warning('assertEqual 1: ' + '[%s>] 100%%\n' % ('=' * 29))
logging.warning('assertEqual 2: ' + output.getvalue())
finally:
sys.stdout = saved_stdout
이곳저곳에 logging 메시지를 달아놨습니다.
logging.warning으로 한 이유는 로그 레벨 기본 설정이 warning 이상(ERROR, FATAL)이 되어야 출력이 되기 때문입니다.
tox -e py unit.test_progressbar.TestProgressBarWrapper.test_iter_file_display_progress_bar
이렇게 수정한 상태로 TestProgressBarWrapper클래스의 test_iter_file_display_progress_bar를 실행시켜보았습니다.
py run-test-pre: PYTHONHASHSEED='3332601188'
py run-test: commands[0] | stestr run --slowest unit.test_progressbar.TestProgressBarWrapper.test_iter_file_display_progress_bar
WARNING:root:test_iter_file_display_progress_bar
WARNING:root:chunk: XXXXXXXXXXXXXX
WARNING:root:chunk: XXXXXXXXXXXXXX
WARNING:root:chunk: XX
WARNING:root:chunk:
WARNING:root:assertEqual 1: [=============================>] 100%
WARNING:root:assertEqual 2: [=============================>] 100%
{0} glanceclient.tests.unit.test_progressbar.TestProgressBarWrapper.test_iter_file_display_progress_bar [0.000520s] ... ok
======
Totals
======
Ran: 1 tests in 0.0005 sec.
- Passed: 1
- Skipped: 0
- Expected Fail: 0
- Unexpected Success: 0
- Failed: 0
Sum of execute time for each test: 0.0005 sec.
==============
Worker Balance
==============
- Worker 0 (1 tests) => 0:00:00.000520
Test id Runtime (s)
--------------------------------------------------------------------------------------------------- -----------
glanceclient.tests.unit.test_progressbar.TestProgressBarWrapper.test_iter_file_display_progress_bar 0.001
_______________________________________________________________________________ summary _______________________________________________________________________________
py: commands succeeded
congratulations :)
chunWARNING:root:chunk: XXXXXXXXXXXXXX를 보면 파일을 우리가 정해준 사이즈 14 만큼 읽는 것을 확인할 수 있습니다. 총 30개의 X 를 읽네요.
assertEqual 1: [=============================>] 100% 를 보았을 때, [%s>] 100%%n' % ('=' * 29) 는 '[=============================>] 100%' 으로 출력이 되는 것을 볼 수 있습니다.
assertEqual 2: [=============================>] 100% 를 보았을 때, output.getvalue() 으로 읽어드린 값은 '[=============================>] 100%' 으로 출력이 되는 것을 볼 수 있습니다.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import sys
import six
from openstackclient.common import progressbar
from openstackclient.tests.unit import utils
class TestProgressBarWrapper(utils.TestCase):
def test_iter_file_display_progress_bar(self):
size = 98304
file_obj = six.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = FakeTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
self.assertEqual(
'[%s>] 100%%\n' % ('=' * 29),
output.getvalue()
)
finally:
sys.stdout = saved_stdout
def test_iter_file_no_tty(self):
size = 98304
file_obj = six.StringIO('X' * size)
saved_stdout = sys.stdout
try:
sys.stdout = output = FakeNoTTYStdout()
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
chunksize = 1024
chunk = file_obj.read(chunksize)
while chunk:
chunk = file_obj.read(chunksize)
# If stdout is not a tty progress bar should do nothing.
self.assertEqual('', output.getvalue())
finally:
sys.stdout = saved_stdout
class FakeTTYStdout(six.StringIO):
"""A Fake stdout that try to emulate a TTY device as much as possible."""
def isatty(self):
return True
def write(self, data):
# When a CR (carriage return) is found reset file.
if data.startswith('\r'):
self.seek(0)
data = data[1:]
return six.StringIO.write(self, data)
class FakeNoTTYStdout(FakeTTYStdout):
"""A Fake stdout that is not a TTY device."""
def isatty(self):
return False
openstackclient/tests/units/fakes.py에 있는 FakeStdout를 사용하려고 했으나,
openstack git에 비슷한 예시가 없었습니다. (못 찾은 것일 수도 있습니다.)
pythonclient 프로젝트 내부에도 class FakeStdout을 새로 정의해서 사용했습니다.
기존의 것을 활용하는 것에 어려움을 겪어서 일단 기존에 있는 코드를 가져왔습니다.
tox -e py openstackclient.tests.unit.common.test_progressbar.TestProgressBarWrapper
py run-test-pre: PYTHONHASHSEED='3879511963'
py run-test: commands[0] | stestr run openstackclient.tests.unit.common.test_progressbar.TestProgressBarWrapper
{0} openstackclient.tests.unit.common.test_progressbar.TestProgressBarWrapper.test_iter_file_display_progress_bar [0.000667s] ... ok
{0} openstackclient.tests.unit.common.test_progressbar.TestProgressBarWrapper.test_iter_file_no_tty [0.000378s] ... ok
======
Totals
======
Ran: 2 tests in 0.0012 sec.
- Passed: 2
- Skipped: 0
- Expected Fail: 0
- Unexpected Success: 0
- Failed: 0
Sum of execute time for each test: 0.0010 sec.
==============
Worker Balance
==============
- Worker 0 (2 tests) => 0:00:00.001192
____________________________________________________________________________________________________________________ summary _____________________________________________________________________________________________________________________
py: commands succeeded
congratulations :)
단위 테스트가 성공한 것을 볼 수 있습니다.
tox
그리고 전체 테스트를 진행하였습니다.
먼저 gerrit에 등록된 아이디랑 연결시켜줍니다. 잘 모르겠으면 다음 링크를 참고하시면 됩니다.
이제 커밋 메시지를 작성하겠습니다.
좋은 커밋 메시지를 작성하는 방법은 wiki 에서 볼 수 있습니다.
task: 40003 story: 2007777
커밋 메시지에 필요하므로, 자신의 스토리보드 숫자와 task 숫자를 잘 기억해둡니다.
Add support '--progress' option for 'image create'
openstack-client doesn’t support the upload progress bar.
This patch shows progressbar when create image
if you added '--progress' option like a python-glanceclient.
like this.
[=============================>] 100%
+------------------+---------------------------+
| Field | Value |
+------------------+---------------------------+
| container_format | bare |
| created_at | 2020-09-06T20:44:40Z |
...
How to use
Add the'--progress' option on the 'openstack image create' command.
Code was written by referring to 'python-glanceclient' project
on stable/ussuri branch
task: 40003
story: 2007777
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is up to date with 'origin/master'.
#
# Changes to be committed:
# new file: openstackclient/common/progressbar.py
# modified: openstackclient/image/v2/image.py
# new file: openstackclient/tests/unit/common/test_progressbar.py
#
그리고 열심히 번역기를 동원해 영어로 커밋 메시지를 작성했습니다.
첫째줄은 50자 이내, 그리고 한 칸을 띄어야 하며, 한 줄은 72자로 줄 바꿈을 했습니다.
git review 를 하면... 첫 리뷰를 올릴 수 있습니다.
review.opendev.org 에 가면 코드가 올라가 있는 것을 확인할 수 있습니다.
제가 올린 review는 review.opendev.org/#/c/750111/ 에서 확인할 수 있습니다.
실패했습니다. osc-functional-devstack을 클릭하면, 왜 실패했는지 확인할 수 있는 페이지로 이동 됩니다.
ft1.1: openstackclient.tests.functional.compute.v2.test_server.ServerTests.test_boot_from_volumetesttools.testresult.real._StringException: Traceback (most recent call last):
File "/home/zuul/src/opendev.org/openstack/python-openstackclient/openstackclient/tests/functional/compute/v2/test_server.py", line 760, in test_boot_from_volume
server_name
File "/home/zuul/src/opendev.org/openstack/python-openstackclient/openstackclient/tests/functional/base.py", line 67, in openstack
fail_ok=fail_ok
File "/home/zuul/src/opendev.org/openstack/python-openstackclient/openstackclient/tests/functional/base.py", line 35, in execute
result_err)
tempest.lib.exceptions.CommandFailed: Command 'openstack --os-cloud=devstack-admin server create -f json --flavor m1.tiny --image cirros-0.5.1-x86_64-disk --boot-from-volume 1 --nic net-id=75b3fb77-e722-41e5-9e8e-b61308e512a5 --wait 875bb1a5035d409bbbf50e77ab83078f' returned non-zero exit status 1.
stdout:
stderr:
b'Quota exceeded for instances: Requested 1, but already used 1 of 0 instances (HTTP 403) (Request-ID: req-1228058d-5c36-4b4a-8303-f38f640a18d1)\n'
ft2.1: openstackclient.tests.functional.volume.v2.test_volume_backup.VolumeBackupTests.test_volume_backup_restoretesttools.testresult.real._StringException: Traceback (most recent call last):
File "/home/zuul/src/opendev.org/openstack/python-openstackclient/openstackclient/tests/functional/volume/v2/test_volume_backup.py", line 56, in test_volume_backup_restore
self.wait_for_status("volume backup", backup['id'], "available")
File "/home/zuul/src/opendev.org/openstack/python-openstackclient/openstackclient/tests/functional/volume/base.py", line 46, in wait_for_status
cls.assertOutput(desired_status, current_status)
File "/home/zuul/src/opendev.org/openstack/python-openstackclient/openstackclient/tests/functional/base.py", line 101, in assertOutput
raise Exception(expected + ' != ' + actual)
Exception: available != restoring
볼륨 백업 테스트와 서버 생성 단계에서 예외사항이 발생한것으로 보입니다. 제가 수정한 코드는 해당 실패사항과 관련이 없다고 생각하기 때문에 다시 검증 작업을 수행해야합니다.
gerrit 사이트로 가서 recheck 라고 댓글을 남기면 다시 zuul이 동작하게 됩니다.
역시 zuul의 오류였습니다. 다시 체크를 하니 테스트 통과했습니다
이후 새롭게 변경 되는 사항이 있으면 글을 쓰도록 하겠습니다.
마지막으로 삽질하고 있을 때 조언과 많은 도움을 주신 오픈스택 컨트리뷰톤 멘토님께 감사드립니다.
tox 는 파이썬의 자동화 테스팅 도구입니다.
이 글은 ubntu 18.04에서 환경에서 진행하였습니다.
pip install tox
먼저 tox를 설치해줍니다.
git clone https://opendev.org/openstack/python-openstackclient
cd python-openstackclient
테스트를 진행할 openstack 프로젝트를 clone 한 뒤에 폴더로 이동해줍니다.
ubuntu@devstack-master:~/python-openstackclient$ cat tox.ini
[tox]
minversion = 3.2.0
envlist = py37,pep8
skipdist = True
# Automatic envs (pyXX) will only use the python version appropriate to that
# env and ignore basepython inherited from [testenv] if we set
# ignore_basepython_conflict.
ignore_basepython_conflict = True
tox.ini 파일을 보면 tox 설정들을 볼 수 있습니다.
tox
tox 를 입력하면 전체 테스트가 돌아갑니다.
tox -e py
tox 만으로 동작하지 않는다면, 위와 같이 py 환경변수를 같이 입력해주세요.
tox -e py37
이런식으로 테스팅 파이썬 환경을 직접 지정해줄 수도 있다고 하는데, python3.7 을 따로 설치 해보았지만 저는 제대로 동작하지 않았습니다. 혹시 테스팅 되는 분은 알려주시면 감사하겠습니다. (py만 주었을 때 가장 잘 작동하는 것으로 보아 확실하지는 않지만 자동으로 환경을 맞추어 실행하는게 아닐까 추측해봅니다.)
One common activity is to just run a single test, you can do this with tox simply by specifying to just run py37 tests against a single test:
tox -e py37 -- cinder.tests.unit.volume.test_availability_zone.AvailabilityZoneTestCase.test_list_availability_zones_cached
Or all tests in the test_volume.py file:
tox -e py37 -- cinder.tests.unit.volume.test_volume
You may also use regular expressions to run any matching tests:
tox -e py37 -- test_volume
출처: docs.openstack.org/cinder/latest/contributor/testing.html , docs.openstack.org/kolla/latest/contributor/running-tests.html
대략 위와 같은 느낌으로 tox -e 파이썬 환경(py,36 py37..)을 입력해주고, 뒤에는 테스트할 파일이나 클래스, 함수 등의 경로를 입력해주면 됩니다.
clone한 pytonh-openstackclinet 에서 테스트를 직접 단위 테스트를 수행해보도록 하겠습니다.
python-openstackclient/openstackclient/tests 위치에서 tree 명령어로 구조를 보면 다음과 같습니다.
stack@server1:~/tmp/python-openstackclient/openstackclient/tests$ pwd
/opt/stack/tmp/python-openstackclient/openstackclient/tests
stack@server1:~/tmp/python-openstackclient/openstackclient/tests$ tree
.
├── __init__.py
├── __pycache__
│ └── __init__.cpython-36.pyc
├── functional
│ ├── __init__.py
│ ├── base.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── test_args.py
│ │ ├── test_availability_zone.py
│ │ ├── test_configuration.py
│ │ ├── test_extension.py
│ │ ├── test_help.py
│ │ ├── test_module.py
│ │ ├── test_quota.py
│ │ └── test_versions.py
│ ├── compute
│ │ ├── __init__.py
│ │ └── v2
│ │ ├── __init__.py
│ │ ├── common.py
│ │ ├── test_agent.py
│ │ ├── test_aggregate.py
만약 우리가 tests 폴더 전체를 테스트하고 싶다고 가정해보겠습니다.
stack@server1:~/tmp/python-openstackclient$ tox -e py openstackclient.tests
stack@server1:~/tmp/python-openstackclient$ tox -e py tests
이와 같이 실행시켜 주면 됩니다.
tox를 실행하는 위치는 같은 프로젝트 내부라면 어디든 상관없습니다.
stack@server1:~/tmp/python-openstackclient$ tox -e py unit.compute.v2.test_server
만약 tests/unit/test_shell.py 파일을 실행시키고자 한다면 위와 같이 작성해주시면 됩니다.
.py 를 제외하고 .을 사용해 경로를 지정해주어야합니다.
stack@server1:~/tmp/python-openstackclient$ tox -e py unit.compute.v2.test_server
TestServerCreate 클래스만을 테스트 하기 위해서는 위와 같이 작성해주시면 됩니다.
stack@server1:~/tmp/python-openstackclient$ tox -e py unit.compute.v2.test_server
TestServerCreate 클래스의 test_server_create_no_options 메소드를 실행키기 위해서는 위와 같이 작성해주시면 됩니다.
여러 곳으로 흩어져 있어서 모아봤습니다.
오픈스택 한국팀에서 운영하는 페이스북 채널
번역 컨트리뷰트 하는 방법부터 시작하여, 오픈스택에 대한 전반적인 내용들을 알 수 있는 유튜브 강의가 있음.
한국어로 번역된 오픈스택 Document 사이트.
오픈스택 트레이닝 문서.
한국어로 되어 있음.
https://docs.openstack.org/ko_KR/upstream-training/upstream-training-content.html
오픈스택 한국 커뮤니티 github 사이트
컨트리뷰톤 활동내역이나, 스터디 활동 등 확인할 수 있음
오픈스택 github 링크
opendev에서 미러링되어 운영되고 있음.
오픈스택에서 사용하는 git 페이지
opendev.org/explore/repos
오픈스택에서는 gerrit 이라는 코드리뷰사이트를 사용하고 있음
오픈스택의 Gerrit에 대해 설명해놓은 문서
https://docs.opendev.org/opendev/system-config/latest/gerrit.html
스토리 보드에 관한 설명 문서
새로운 기능이나 버그 수정과 같은 스토리를 찾을 수 있다.
스토리 보드와 비슷한 역할을 하나 최근에는 스토리보드를 더 많이 사용
launchpad.net/openstack
오픈스택 자나타 홈페이지
번역 기여을 할 수 있다. 기여방법은 블로그에 작성.
개발자 가이드, gerrit workflow 등 코드 컨트리뷰트 방법에 대한 문서
https://docs.opendev.org/opendev/infra-manual/latest/index.html
devstack을 설치방법에 대해 나와있는 문서
오픈스택을 운영하는 조직 구성에 대해 전반적으로 알 수 있음.
오픈스택에서는 커뮤니케이션에 주로 IRC(인터넷 릴레이 챗)를 사용하는데 이러한 챗 기록들과 채널들을 확인 할 수 있음.
이미지
저는 좀 더 손쉽게 로컬 환경에서 분석을 하기 위해 pycham을 사용하여 서버에 있는 /usr/local/lib/python3.6/dist-packages/openstackclient/ 와 직접 동기화 한 뒤 진행하였습니다. (디버깅 모드를 이용하면 위에 보이는 것처럼 argv의 값을 직접 볼 수 있습니다.)
또한 여기에서 분석한 코드는 https://github.com/openstack/python-openstackclient/tree/5.2.0 입니다.
tag 5.2.0입니다.
오픈스택을 설치하고, openstack server list를 치게 되면 위 그림처럼 이쁘게 출력됩니다..
명령어는 어떻게 전달되고,
어디서 이쁘게 출력되는지 알아보려고 합니다.
먼저 오픈스택 명령어의 위치를 알아냅니다. which를 찍으면 어디서에 실행되었는지 알 수 있습니다. 해당 파일을 열어보겠습니다.
stack@server1:~$ cat /usr/local/bin/openstack
#!/usr/bin/python3.6
# -*- coding: utf-8 -*-
import re
import sys
from openstackclient.shell import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
openstackclient.shell의 main을 import 하고 있는 것을 볼 수 있습니다. 그리곤 단순히 main()만을 실행합니다.
main()으로 직접 가보겠습니다.
stack@server1:~$ python3
Python 3.6.9 (default, Jul 17 2020, 12:50:27)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import openstackclient
>>> openstackclient
<module 'openstackclient' from '/usr/local/lib/python3.6/dist-packages/openstackclient/__init__.py'>
openstackclient 모듈의 위치는 /usr/local/lib/python3.6/dist-packages/openstackclient/ 에 위치해 있네요.
openstackclient의 shell.py를 열어 main()을 찾아보겠습니다.
# /usr/local/lib/python3.6/dist-packages/openstackclient/shell.py
from osc_lib import shell
"""생략 """
class OpenStackShell(shell.OpenStackShell):
def __init__(self):
super(OpenStackShell, self).__init__(
description=__doc__.strip(),
version=openstackclient.__version__,
command_manager=commandmanager.CommandManager('openstack.cli'),
deferred_help=True)
"""생략 """
def main(argv=None):
"""생략 """
return OpenStackShell().run(argv) #argv: ['server', 'list']
main 함수의 마지막 부분을 보면 OpenStackShell클래스의 run()을 실행하는 것을 볼 수 있습니다.
OpenStackShell은 shell.OpenStackShell을 상속받고 있으며, 자체적으로 run()을 가지고 있지는 않네요. 아마 부모 클래스의 run()을 사용하는 것 같습니다.
osc_lib.OpenStackShell.run()이 어디에 있는지 찾아보겠습니다.
stack@server1:/usr/local/lib/python3.6/dist-packages/openstackclient$ python3
Python 3.6.9 (default, Jul 17 2020, 12:50:27)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import osc_lib
>>> osc_lib
<module 'osc_lib' from '/usr/local/lib/python3.6/dist-packages/osc_lib/__init__.py'>
또 다른 폴더입니다. 이와 같은 방법으로 선언된 부모 클래스를 찾아갈 수 있습니다.
ocs_lib의 shell.run() 위치를 찾아보겠습니다.
# /usr/local/lib/python3.6/dist-packages/osc_lib/shell.py
class OpenStackShell(app.App):
def run(self, argv):
ret_val = 1
self.command_options = argv
try:
ret_val = super(OpenStackShell, self).run(argv) #argv: ['server', 'list']
return ret_val
except Exception as e:
'''생략'''
해당 클래스는 app.py의 App클래스를 상속받아 구현된 것을 볼 수 있습니다.
ret_val = super(OpenStackShell, self).run(argv) #argv: ['server', 'list']
부모 클래스의 run()을 실행한 것을 볼 수 있습니다.
app.py의 App클래스의 run()으로 이동해보겠습니다.
# /usr/local/lib/python3.6/dist-packages/cliff/app.py
class App(object):
''' 생략 '''
def run(self, argv): #argv: ['server', 'list']
try:
self.options, remainder = self.parser.parse_known_args(argv) # remainder['server', 'list']
self.configure_logging()
self.interactive_mode = not remainder
if self.deferred_help and self.options.deferred_help and remainder:
remainder.insert(0, "help")
self.initialize_app(remainder)
self.print_help_if_requested()
except Exception as err:
''' 생략 '''
result = 1
if self.interactive_mode:
result = self.interact()
else:
result = self.run_subcommand(remainder) # remainder['server', 'list']
return result
rgv에 ['server', 'list'] 값이 들어있고, parser.parse_known_arg에 의해 remainder에 ['server', 'list']가 담기게 됩니다.
else:
result = self.run_subcommand(remainder) # remainder['server', 'list']
우리는 인터렉티브 모드로 실행하지 않았기 때문에 else문에 들어가게 되는데, run_subcommand() 함수로 인자 값을 넘겨줍니다.
app.py의 APP.run_subcommand() 따라가 보겠습니다.
# /usr/local/lib/python3.6/dist-packages/cliff/app.py
class App(object):
''' 생략 '''
def run_subcommand(self, argv): #argv: ['server', 'list']
try:
subcommand = self.command_manager.find_command(argv)
''' 생략 '''
return result
run_subcommand() 또한 App클래스 내부에 있는 함수입니다. 우리가 넘겨준 remainder 값들을 여기에서는 argv로 받고 있습니다.
그리곤 command_manage.find_command()에 argv를 인자로 넘겨주고 있는 것을 볼 수 있습니다.
아마 여기에서 커맨드를 찾는 거 같군요.
app.py의 CommandManager.find_command()로
# /usr/local/lib/python3.6/dist-packages/cliff/app.py
class CommandManager(object):
''' 생략 '''
def find_command(self, argv):
start = self._get_last_possible_command_index(argv)
for i in range(start, 0, -1):
name = ' '.join(argv[:i])
search_args = argv[i:]
return_name = name
if name in self._legacy:
name = self._legacy[name]
found = None
if name in self.commands:
found = name
else:
candidates = _get_commands_by_partial_name(
argv[:i], self.commands)
if len(candidates) == 1:
found = candidates[0]
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 = utils.getargspec(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)
CommandManager라고 하니, 커맨드를 찾는 관리하는 친구인 거 같습니다, 그리고 최종적으로 cmd_factory, return_name, search_args를 반환합니다.
각각 값들을 보면 다음과 같이 담겨서 반환됩니다.
클래스 객체를 cmd_factory에 담을 수 있던 걸까요?
정답부터 알려드리면,,,
CommandManger클래스가 초기화될 때, self.commands에 EntryPoint라는 객체들이 저장됩니다.
# /usr/local/lib/python3.6/dist-packages/python_openstackclient-5.2.0.dist-info/entry_points.txt'
server_group_show = openstackclient.compute.v2.server_group:ShowServerGroup
server_image_create = openstackclient.compute.v2.server_image:CreateServerImage
server_list = openstackclient.compute.v2.server:ListServer
server_lock = openstackclient.compute.v2.server:LockServer
이 값들은 '/usr/local/lib/python3.6/dist-packages/python_openstackclient-5.2.0.dist-info/entry_points.txt'에 존재하는데 여기에 저장되어 있는 값을 이용하여 '_'를 ' '로 교체한 뒤 self.commands에 저장합니다. (server_list -> server list)
그리고 불러온 커맨드 목록들 중에 'server list'를 찾아서 cmd_factory에 저장하게 됩니다.
블랙홀에 빠지기 싫으시면 5번으로 패스하시면 됩니다.
if found: # found: 'server list'
cmd_ep = self.commands[found]
'server list' 값을 찾은 결과 cmd_ep에 값이 담기는 것을 볼 수 있습니다.
그리고 계속 디버깅하다 보면 dist에 python-openstackclient 5.2.0이라는 값이 보이는데,
self.commands값을 직접 살펴봤습니다. 'server list'도 있는 것을 볼 수 있습니다.
그렇다면 self.command에 있는 server list는 어디서 찾은 걸까??
그래서 CommandManager의 클래스 초기화 부분으로 가보았습니다.
# /usr/local/lib/python3.6/dist-packages/cliff/app.py
class CommandManager(object):
'''생략'''
def __init__(self, namespace, convert_underscores=True):
self.commands = {}
self._legacy = {}
self.namespace = namespace
self.convert_underscores = convert_underscores
self._load_commands()
def _load_commands(self):
# NOTE(jamielennox): kept for compatibility.
if self.namespace:
self.load_commands(self.namespace)
def load_commands(self, namespace):
"""Load all the commands from an entrypoint"""
for ep in pkg_resources.iter_entry_points(namespace):
LOG.debug('found command %r', ep.name)
cmd_name = (ep.name.replace('_', ' ')
if self.convert_underscores
else ep.name)
self.commands[cmd_name] = ep
return
'''생략'''
def load_commands()에서 커맨드를 찾고 값들을 replace 하는 것을 볼 수 있습니다.
page_resources.iter_entry_points()에서 나온 값을 commands에 저장하는 것을 볼 수 있습니다.
# /usr/local/lib/python3.6/dist-packages/openstackclient/shell.py
class OpenStackShell(shell.OpenStackShell):
def __init__(self):
super(OpenStackShell, self).__init__(
description=__doc__.strip(),
version=openstackclient.__version__,
command_manager=commandmanager.CommandManager('openstack.cli'),
deferred_help=True)
namesapce를 인자 값으로 전달을 하는데, shell.py에서 init 할 때 넘겨준 값인 것을 볼 수 있습니다.
page_resources.iter_entry_points()로 이동해보겠습니다.
def iter_entry_points(self, group, name=None): #group: openstack.cli
return (
entry
for dist in self
for entry in dist.get_entry_map(group).values()
if name is None or name == entry.name
)
열심히 dist 폴더들을 돌면서 'openstack.cli' 값이 있는지 찾아보고 있는 것으로 보입니다. (추측)
아마 entry_points.txt의 파일 내부에 있는 [openstack.cli]를 찾고 있는 거 같습니다.
그리고 계속 디버깅하다 보면 dist에 python-openstackclient 5.2.0이라는 값이 보이는데,
dist객체의 egg_info 정보를 보면 path가 있는 것을 볼 수 있습니다.
stack@server1:~$ sudo find /usr/local/lib/python3.6/dist-packages/python_openstackclient-5.2.0.dist-info/ -name "*.*" -type f | xargs grep "server_list"
/usr/local/lib/python3.6/dist-packages/python_openstackclient-5.2.0.dist-info/entry_points.txt:server_list = openstackclient.compute.v2.server:ListServer
그래서 해당 폴더로 가서 검색을 해보았습니다. entry_points.txt에서 server_list가 검색이 된 것을 확인할 수 있었습니다.
entrypoint.txt 에서 데이터를 읽는 것으로 보입니다.
찾고 나면 cmd_name에 moude_list를 변환하여 cmd_name으로 변환한 뒤 하나씩 self.command값들을 추가시키는 것으로 보입니다.
실제 데이터를 확인해보면 한번 for문을 돌 때마다 값이 추가되는 것을 볼 수 있었습니다.
어떻게 도는지 제대로 파악을 못했습니다. 또한 삽질을 하면서 느낌적으로 분석한 거라 잘못된 부분이 충분히 있을 수 있습니다. (있을 겁니다.)
# /usr/local/lib/python3.6/dist-packages/cliff/app.py
class CommandManager(object):
''' 생략 '''
def find_command(self, argv):
''' 생략 '''
if found:
cmd_ep = self.commands[found]
if hasattr(cmd_ep, 'resolve'):
cmd_factory = cmd_ep.resolve()
''' 생략 '''
return (cmd_factory, return_name, search_args)
cmd_factory에 ListServer클래스가 반환됩니다.
# /usr/local/lib/python3.6/dist-packages/cliff/app.py
class App(object):
''' 생략 '''
def run_subcommand(self, argv): #argv: ['server', 'list']
try:
subcommand = self.command_manager.find_command(argv)
except ValueError as err:
''' 생략 '''
cmd_factory, cmd_name, sub_argv = subcommand
kwargs = {}
if 'cmd_name' in utils.getargspec(cmd_factory.__init__).args:
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self, self.options, **kwargs)
result = 1
드디어 CommandManager() 클래스를 빠져나올 수 있게 되었습니다. 이곳을 호출한 APP클래스의 run_subcommand로 되돌아왔습니다.
(cmd_factory, return_name, search_args)를 subcommand로 받은 뒤, (cmd_factory, cmd_name, sub_argv)로 다시 받습니다.
if 'cmd_name' in utils.getargspec(cmd_factory.__init__).args:
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self, self.options, **kwargs)
그리고는 cmd_factory를 어떠한 가공(?)을 거쳐 cmd 변수를 새롭게 만드는데, 이 부분은 잘 모르겠습니다.
계속 한 줄씩 실행하겠습니다.
cmd = cmd_factory(self, self.options, **kwargs)
result = 1
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name
if self.interactive_mode
else ' '.join([self.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
prepare_to_run_command(cmd)를 하는데, 저 함수는 비어있고 설명만 적혀 있습니다.
full name을 받는데, 우리는 interactive_mode가 아니므로 openstack server list가 반환된다. 만약 인터렉티브 모드로 동작했을 때 차이점이 여기에서 나타나는 듯합니다.
그리고 cmd.get_parser()가 동작을 하는데, 이는 아까 만들었던 cmd 객체라고 볼 수 있다. 즉, 여기에서 ListServer.get_parser()로 들어간다는 것을 볼 수 있습니다.
# /usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py
class ListServer(command.Lister):
_description = _("List servers")
def get_parser(self, prog_name):
parser = super(ListServer, self).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들을 추가시킨다. 여기에서 추가된 값들은 우리가 흔히 보는 커맨드에서 출력하는 의미하는 것 같습니다.
커맨드 라인에 이런 식으로 추가되는 거 같은데, 잘 모르겠습니다. 추가적인 공부를 해야 할 듯합니다.
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
cmd.run()이 실행되게 되는데,
이곳에서 실행되면서 커맨드 창에 그림이 그려지고, 서버에 요청을 해서 데이터를 얻어오게 됩니다.
cmd.run을 따라 가보겠습니다.
@six.add_metaclass(CommandMeta)
class Command(command.Command):
def run(self, parsed_args):
self.log.debug('run(%s)', parsed_args)
return super(Command, self).run(parsed_args)
Command.run()을 거쳐(잘 몰라서,, 패스)
# /usr/local/lib/python3.6/dist-packages/cliff/display.py
@six.add_metaclass(abc.ABCMeta)
class DisplayCommandBase(command.Command):
'''생략'''
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
DisyplayCommandBase.run()이 실행됩니다.
column_names, data = self.take_action(parsed_args)
self.produce_output(parsed_args, column_names, data)
여기에서 주목해야 할 곳은 take_action()과 produce_output()입니다. 특히 더 주목해야 되는 곳은 take_action()
take_action()에서는 서버에 데이터를 요청 및 가공을 하고, 그 결과 값을 column_name, data에 담습니다.
produce_output()에서는 take_action()에서 column_name, data을 토대로 실제 콘솔에 그림을 그립니다.
take_action()부터 보겠습니다.
# /usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py
class ListServer(command.Lister):
_description = _("List servers")
'''생략'''
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity
image_client = self.app.client_manager.image
'''생략'''
data = compute_client.servers.list(search_opts=search_opts,
marker=marker_id,
limit=parsed_args.limit)
'''생략'''
table = (column_headers,
(utils.get_item_properties(
s, columns,
mixed_case_fields=mixed_case_fields,
formatters={
'OS-EXT-STS:power_state':
_format_servers_list_power_state,
'Networks': _format_servers_list_networks,
'Metadata': utils.format_dict,
},
) for s in data))
return table
table이라는 튜플이 리턴되는 것을 볼 수 있습니다.
table값을 보면 column_headers와 generator이 서로 쌍으로 들어있는 것을 볼 수 있습니다.
column_headers이 만만해 보이므로 먼저 접근해보았습니다. column_headers는 이게 끝입니다.
(utils.get_item_properties(
s, columns,
mixed_case_fields=mixed_case_fields,
formatters={
'OS-EXT-STS:power_state':
_format_servers_list_power_state,
'Networks': _format_servers_list_networks,
'Metadata': utils.format_dict,
},
) for s in data)
이제 column_headers 오른쪽에 있는 복잡한 녀석을 한번 접근해 보겠습니다. get_item_properties에 인자 값으로 s, columns, mixed_case_fields, formatters를 전달해줍니다.
def get_item_properties()로 가보겠습니다.
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
''' 생략 '''
row = []
for field in fields: # field: 'ID'
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_') # field_name: 'id'
data = getattr(item, field_name, '')
if field in formatters:
formatter = formatters[field]
fields에서 하나씩 꺼내온 다음 '_'를 ' '로 변환하고, 대문자를 소문자로 변환하는 과정을 거치는 것을 볼 수 있습니다.
파이참의 디버깅 모드에서는 중간에 디버그 걸린 곳의 변수를 직접 사용할 수 있습니다. 이를 이용해 데이터 값을 조회해 봤습니다.
item에 들어있는 id를 가져와서 data라는 변수에 넣은 것을 확인할 수 있습니다.
field의 Flavar Name을 뽑아서 추출할 경우도 m1.nano가 data에 담깁니다.
item.flavor_name으로도 데이터를 볼 수 있습니다.
item에 존재하는 다른 값들도 넣어보겠습니다. 잘 출력이 됩니다. item에 존재하는 다른 값들도 꺼내 올 수 있겠네요.
다시 table을 만드는 튜플로 돌아가 보겠습니다.
table = (column_headers,
(utils.get_item_properties(
s, columns,
mixed_case_fields=mixed_case_fields,
formatters={
'OS-EXT-STS:power_state':
_format_servers_list_power_state,
'Networks': _format_servers_list_networks,
'Metadata': utils.format_dict,
},
) for s in data))
item은 여기에서 s이고 s는 data에서 뽑아온 값인 것을 확인할 수 있습니다.
여기에서 columns은 fields인 것을 확인할 수 있습니다.
columns는 다음과 같이 정의되어 있는 것을 확인해볼 수 있습니다.
data는 여기에서 새로 받네요.
궁금해서 잠깐 compute_client.servers.list()로 들어가 봤습니다. (novaclient로 요청하는 부분은 나중에 분석할 기회가 생기면 자세히 추가시켜 놓겠습니다.)
# novaclient/client.py
def request(self, url, method, **kwargs):
kwargs.setdefault('headers', kwargs.get('headers', {}))
api_versions.update_headers(kwargs["headers"], self.api_version)
# NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
# headers in case if osprofiler is not initialized.
if osprofiler_web:
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
# NOTE(jamielennox): The standard call raises errors from
# keystoneauth1, where we need to raise the novaclient errors.
raise_exc = kwargs.pop('raise_exc', True)
with utils.record_time(self.times, self.timings, method, url):
resp, body = super(SessionClient, self).request(url,
method,
raise_exc=False,
**kwargs)
servers.list를 잠깐 들어가 보니까 url로 요청을 보내는 곳이 있는 거 같습니다. Get method도 있네요.
요청해서 받은 응답 헤더와 body를 보니까 200과 서버 데이터들이 들어 있는 것을 볼 수 있습니다. (나중에 살펴봐야겠네요)
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)
이렇세 take_action은 끝입니다.
table이 반환이 되지만 튜플 형태로 반환되기 때문에, column_names와 data 각각 나눠서 담기는 것을 확인할 수 있었습니다.
column_names, data = self._run_after_hooks(parsed_args,
(column_names, data))
self.produce_output(parsed_args, column_names, data)
produce_output에서는 그림을 실제로 콘솔 창에 출력을, take_action에서 반환받은 column_names정보와 data정보를 가지고 출력을 합니다.
produce_output의 내부로 진행하다 보면 TableFormatter.emit_list()가 나옵니다.
# cliff/formatters/Table.py
class TableFormatter(base.ListFormatter, base.SingleFormatter):
''' 생략 '''
def emit_list(self, column_names, data, stdout, parsed_args):
x = prettytable.PrettyTable(
column_names,
print_empty=parsed_args.print_empty,
)
x.padding_width = 1
# Add rows if data is provided
if data:
self.add_rows(x, column_names, data)
# Choose a reasonable min_width to better handle many columns on a
# narrow console. The table will overflow the console width in
# preference to wrapping columns smaller than 8 characters.
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
formatted = x.get_string()
stdout.write(formatted)
stdout.write('\n')
x.get_string()로 formatteed 데이터를 넣고, stdout.write로 데이터를 출력하는 것을 볼 수 있습니다.
여기에서 이쁘게 출력되네요. (2. 어디서 이쁘게 출력되는지 알아보려고 합니다.)
TableFormatter에서 이쁘게 만들기 위해 여러 노력하는 거 같은데 더 깊게는 안 가봤습니다.
그리고는 터미널에 결과가 출력이 됩니다.
첫번째 모임에서 실습한 devstack설치 방법을 정리해보고자 문서화하였습니다.
준비물: ubuntu 18.04 LTS, 램 8기가 이상의 서버, 30분이라는 시간, 80 포트가 열려있는 방화벽
sudo useradd -s /bin/bash -d /opt/stack -m stack
echo "stack ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/stack
sudo su - stack
git clone https://opendev.org/openstack/devstack
cd devstack
여기에서 tag를 보고 설치하고 싶은 버젼을 고르면 checkout 하면 됩니다.
ifconfig
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1450
inet 192.168.1.8 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::f816:3eff:fe99:c461 prefixlen 64 scopeid 0x20<link>
ether fa:16:3e:99:c4:61 txqueuelen 1000 (Ethernet)
RX packets 5069 bytes 12678483 (12.6 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4725 bytes 434212 (434.2 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
192.168.1.8을 기억해둡니다.
vim local.conf
[[local|localrc]]
ADMIN_PASSWORD=secret
DATABASE_PASSWORD=$ADMIN_PASSWORD
RABBIT_PASSWORD=$ADMIN_PASSWORD
SERVICE_PASSWORD=$ADMIN_PASSWORD
HOST_IP=192.168.1.8
HOST_IP에 내 내부 아이피를 적어줍니다.
./stack.sh
설치 해줍니다.
./unstack.sh
./clean.sh
설치 실패 시 설정을 수정한 뒤에 unstack.sh 을 실행 후 다시 stack.sh 을 실행해줍니다. (서버를 다시 만드는 것도 좋은 방법..)
그래도 안되면 clean.sh 으로 다 데이터를 밀어버립시다.
./unstack.sh 시에 적용되는 것
- Stopping the project services, mysql and rabbitmq
- Cleaning up iSCSI volumes
- Clearing temporary LVM mounts
./clean.sh 시에 적용되는 것 (unstack.sh 후에 사용)
- Removing configuration files for projects from /etc
- Removing log files
- Hypervisor clean-up
- Removal of .pyc files
- Database clean-up
- etc.
공인 아이피로 접속한 다음
아이디: admin 혹은 demo
패스워드: secret
를 입력해주시면 접속됩니다.
참고 링크:
openstack.dooray.com/share/posts/h-MfTmzOS9S-BaOvYMKjgQ
docs.openstack.org/devstack/latest/
docs.openstack.org/contributors/code-and-documentation/devstack.html
launchpad id : epicarts
zanata id : epicarts
stackanalytics pr : https://github.com/stackalytics/default_data/pull/216
$ openstack server resize --flavor m1.small --wait vm1
명령어 실행이 실패 할 경우 실패 메세지가 떠야할꺼 같지만 실제로는 Complete 메세지가 나오며 실제 flavor를 확인하는 명령어 실행 시
$ openstack server show vm1 -f value -c status -c flavor
m1.tiny (1)
ACTIVE
처럼 이전 상황으로 돌아가는 현상이 있다.
서버의 사양을 뛰어넘는 크기로 flavor를 바꿔본다 제공된 cafe24 서버의 사양은 CPU 4코어 / RAM 8GB / SSD 30GB이다 이때 CPU 8코어 / RAM 16GB / 디스크 160GB 가 필요한 m1.xlarge flavor 로 변경을 진행해 본다.
$ openstack server resize --flavor m1.xlarge --wait vm1
Complete
에러가 발생해야할것으로 예상 되지만 Complete 메세지가 뜬것을 확인할 수 있다.
확인을 위해 현재 flavor를 확인해보면
$ openstack server show vm1 -f value -c status -c flavor
m1.tiny (1)
ACTIVE
와 같이 변경 전 상태임을 확인 할 수 있다.
if parsed_args.wait:
if utils.wait_for_status(
compute_client.servers.get,
server.id,
success_status=['active', 'verify_resize'],
callback=_show_progress,
):
self.app.stdout.write(_('Complete\n'))
else:
LOG.error(_('Error resizing server: %s'),
server.id)
self.app.stdout.write(_('Error resizing server\n'))
raise SystemExit
utils.wait_for_status(
compute_client.servers.get,
server.id,
success_status=['active', 'verify_resize'],
callback=_show_progress,
):
확인 결과 flavor가 변경되지 않은 상태에서도 success_status가 active로 나와 위 코드의 리턴값이 true로 나온다. 이때 정상적으로 flavor가 변경이 되었다면 success_status가 verify_resize로 나오게 된다. 만약 verify_resize가 아니라 active인 상태에서 flavor가 변경되었는지 아닌지를 확인해 보면 해결이 가능할 것 같다.
server
객체에 지금 서버의 flavor id 가 있는것을 확인하였고
flavor
객체에 지금 바꾸려고 하는 flavor id 가 있는것을 확인하였다.
따라서 서버의 verify_resize
상태만 확인이 가능하면
if flavor.id != server_flavor_id:
self.app.stdout.write(_('Error resizing server\n'))
위 코드로 해결이 가능하다.
launchpad id : epdlemflaj
zanata id : silica_jung
stackanalytics pr : https://github.com/stackalytics/default_data/pull/210
첫 컨트리뷰션이 될 이슈를 이것 으로 결정했다.
오픈스택 대시보드의 설정 화면에서 비밀번호를 변경하는 경우, 500 Internal Server Error
가 발생한다. 문제를 찾기 위해 오픈스택 대시보드를 관리하는 프로젝트인 horizon의 로그 (/var/log/apache2/horizon_error.log) 를 확인했다.
2020-09-05 07:55:33.775492 DEBUG keystoneauth.session GET call to identity for http://192.168.1.10/identity/v3/users/a6afb486ad9b4200a4ed37b6865f4e65 used request id req-7d8bed77-9849-4d5e-8a6c-5963bffbfea4
2020-09-05 07:55:33.775714 DEBUG openstack_dashboard.api.keystone Creating a new keystoneclient connection to http://192.168.1.10/identity/v3.
2020-09-05 07:55:33.835818 mod_wsgi (pid=18843): Exception occurred processing WSGI script '/opt/stack/horizon/openstack_dashboard/wsgi.py'.
2020-09-05 07:55:33.835884 Traceback (most recent call last):
2020-09-05 07:55:33.835899 File "/usr/local/lib/python3.6/dist-packages/django/core/handlers/wsgi.py", line 150, in __call__
2020-09-05 07:55:33.835901 start_response(status, response_headers)
2020-09-05 07:55:33.835910 ValueError: unicode object contains non latin-1 characters
로그를 확인하니 값을 넘겨줄 때 어디선가 인코딩에 문제가 생기는 것 같아 보였다. 아니나 다를까 대시보드의 언어 설정을 영어로 하면 아무 문제 없이 잘 동작하고, 한국어 및 다른 언어로 하면 500 에러가 발생했다.
오픈스택 대시보드에서 패스워드 변경시 horizon 프로젝트에서 호출되는 코드는 아래와 같다. (openstack_dashboard/dashboards/settings/password/forms.py)
@sensitive_variables('data')
def handle(self, request, data):
user_is_editable = api.keystone.keystone_can_edit_user()
user_id = request.user.id
user = api.keystone.user_get(self.request, user_id, admin=False)
options = getattr(user, "options", {})
lock_password = options.get("lock_password", False)
if lock_password:
messages.error(request, _('Password is locked.'))
return False
if user_is_editable:
try:
api.keystone.user_update_own_password(request,
data['current_password'],
data['new_password'])
response = http.HttpResponseRedirect(settings.LOGOUT_URL)
msg = _("Password changed. Please log in again to continue.")
utils.add_logout_reason(request, response, msg)
return response
except Exception as ex:
exceptions.handle(request,
_('Unable to change password: %s') % ex)
return False
else:
messages.error(request, _('Changing password is not supported.'))
return False
문제가 발생하는 부분은 위 파일의 77번 라인인 utils.add_logout_reason(request, response, msg)
이다. 코드에서는 위 함수로 메시지를 넘겨주게 되고, 해당 코드를 따라가 보면 아래와 같다.
(horizon/utils/functions.py)
def add_logout_reason(request, response, reason, status='success'):
# Store the translated string in the cookie
lang = translation.get_language_from_request(request)
with translation.override(lang):
reason = str(reason)
response.set_cookie('logout_reason', reason, max_age=10)
response.set_cookie('logout_status', status, max_age=10)
코드에서는 사용자별로 설정해둔 언어에 따라서 전달받은 메시지를 번역하고 그 메시지를 쿠키에 담는다. 이게 에러가 발생하는 원인이 되는데, 그 이유는 쿠키에 번역된 메시지(한글)가 담기기 때문에 django에서 해석을 못 하게 되면서 500 에러가 발생하는 것으로 보인다.
메시지를 미리 인코딩해서 쿠키에 담기
메시지를 꺼내는 부분을 건드려 보기
launchpad id : melkitan
stackanalytics pr : stackalytics/default_data#215
launchpad id : chrm7070
zanata id : choikyungyong
stackanalytics pr : https://github.com/stackalytics/default_data/pull/220
Devstack을 설치하고, CLI 코드를 분석하며 분석능력을 다짐
1.Devstack 설치 (Stable/ussuri || Master) 2개 2.Devstack 대쉬보드 소개
설치환경 : Cafe 24 Hosting을 사용한 가상 서버
OS : Ubuntu 18.04 LTS
HW사양 : m1.xlarge(CPU-4/RAM-8G/SSD-30G)
가상서버에 SSH로 접속하기 위해서는 방화벽이 필요합니다.
기본 정책은 차단되어 있으므로 Inbound Rule 에 SSH port(22)를 추가해줍니다.
접속에 앞서 #01 에서 발급받은 공개키[pem]를 개인키[ppk] 로 바꿔줍니다
그러한 까닭은, EC2 같은 인스턴스 들이 일반적으로 PPK를 사용하기 때문입니다.
변환은 PuTTY Key Generator 를 사용합니다.
PuTTY를 사용하여 생성된 가상서버의 공인IP 에 접속한다. 접속시 SSH에 ppk 키를 입력해준다.
login 시 ubuntu를 입력한다.
공식문서의 설치가이드 는 다음과 같다.
서술할 본문은 공식문서의 내용과 같다.
$ sudo useradd -s /bin/bash -d /opt/stack -m stack
$ echo "stack ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/stack
$ sudo su - stack # Download Devstack
$ git clone https://opendev.org/openstack/devstack
$ cd devstack # Master || Stable/ussuri
$ git checkout stable/ussuri
$ git status
Create local.conf
vim local.conf #생성
[[local|localrc]]
ADMIN_PASSWORD=secret
DATABASE_PASSWORD=$ADMIN_PASSWORD
RABBIT_PASSWORD=$ADMIN_PASSWORD
SERVICE_PASSWORD=$ADMIN_PASSWORD
HOST_IP=[호스팅한 가상서버의 Local IP 입력]
Start Install
./stack.sh #약 20분동안 설치가 진행된다.
성공적으로 설치된 모습
설치가 완료되면 localhost의 80 port를 이용하여 OpenStack의 DashBoard에 접근할 수 있는데, 우리는 외부에서 접속하므로 cafe24 vm의 공인IP를 통해 접속합니다.
이를 위해 다시 cafe24의 방화벽에 Inbound 정책으로 80/6080번 포트를 추가해줍니다. 공인아이피로 대쉬보드에 접속해줍니다.
devstack이 설치되면 총 3개 프로젝트를 기본적으로 만듭니다. * admin * alt_demo(보조데모) * demo
이후에는 Devstack 상에서 Instance를 생성해보겠습니다.
좌측 프로젝트바의 네트워크>네트워크 토폴로지를 클릭합니다.
devstack은 demo 프로젝트에 총 3개의 네트워크를 미리 만들어줍니다.
인터넷과 연결할 수 있는 네트워크 입니다. 지구본 모양이 있으면 인터넷과 연결이 가능합니다.
인스턴스가 직접 연결하는 네트워크 대역이 아닌, floating ip 나 router에 할당하는 아이피입니다.
demo project만의 가상 네트워크입니다.
기본적으로 라우터에 연결되어있어서, 인터넷 통신이 가능한 환경입니다.
프로젝트 바의 인스턴스를 클릭합니다.
소스 : OS를 선택합니다. 오픈스택은 Cirros라는 기본 OS가 있습니다. 다른 OS를 설치하고 싶으면 이미지 배포판을 준비하면 됩니다. 이것은 Glance에 올라갑니다.
Flavor : HW사양 (CPU/RAM/SSD)를 선택합니다
네트워크 : 네트워크 토폴로지의 어느 망과 연결할지 선택합니다.
Key : SSH 공개키를 생성합니다
인스턴스 생성버튼을 누릅니다.
인스턴스 생성과정이 cafe24의 가상서버(vm) 생성과정과 동일합니다.
생성한 개인키를 복사하여 Devstack 서버에 저장해줍니다.
pem키를 이용해서 접속하기 위해서는 권한 설정이 필요합니다.
$ chmod 600 keykey.pem
콘솔 로그를 통해 생성된 인스턴스가 CLI(시리얼)에서 어떻게 동작하고 있는지 볼 수 있습니다.
cafe24의 대쉬보드에서 VM에 22port로 Console로 접속했던 것 처럼 DevStack VM안의 VM Instance에 Console로 접속해봅니다
기본적으로 열리는 주소가 사설주소이기 때문에 아이피를 vm의 공인 IP(DevStack의 IP)로 변경합니다.
http://(vm 의 공인 IP):6080/vnc_lite.html?path=%3Ftoken%3D401a7596-fd37-4cce-8315-2c355275fadf&title=test(baad8346-1541-4f12-a7f3-a41681398679)
이번에는 * 공인 IP 변경을 위해 아래와 같이 Floating IP를 할당해줍니다. * 생성한 가상 환경을 담당하는 OpenStack의 VM이 Floating IP를 Matching 시켜줍니다.
IP대역에 공인IP와 사설IP가 보이면 성공입니다.
마찬가지로, 접속하기전 방화벽(F/W)에서 SSH port(22)를 열어주어야 합니다.
추가한 다음 devstack VM Console(카페24 호스팅 된 가상서버-Ubuntu)에서 devstack VM 위에 설치한 OpenStack VM(Cirros OS)에 접속을 시도합니다.
ubuntu@devstack-ussuri:~$ ssh cirros@172.24.4.3
The authenticity of host '172.24.4.3 (172.24.4.3)' can't be established.
ECDSA key fingerprint is SHA256:U7azykVg/zGNVKHPloX1mXoLUUUjuoz81q5RgLtDkaw.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.24.4.3' (ECDSA) to the list of known hosts.
cirros@172.24.4.3's password
접속 성공한 화면
이러한 유동 IP 상태에서, 방화벽에서 ICMP를 풀어주면 카페24의 VM(Ubuntu)에서 devstack vm에 ping을 보낼 수 있습니다.
[SOSCON 2019] NHN은 어떻게 OpenStack으로 Public Cloud를 운영할까? - 조성수
우리가 할 Openstack은 IaaS (Infra as a Service)입니다. IaaS에는 5가지 필수요소가 있습니다. 어떤 것을 이용하냐에 따라서 VM, Vbox, 클라우드 서비스가 될 수 있습니다.
오픈스택도 이런것을 다 지원하는 개념[오픈스택 아키텍쳐]들이 있으며, 필수요소 외에도 여러 Component 들이 많지만 오늘은 OpenStack의 5가지 필수요소를 알아봅니다.
인스턴스(Nova) : 하나하나가 각각의 서비스로 보시면 돼요
스토리지(Cinder) : 하드디스크
OS이미지(Glance)
네트워크(Neutron) : 인스턴스 間의 가상 네트워크
유저/인증(Keystone)
핵심 컴포넌트는 크게 2가지
API성 서비스 : 각각의 컴포넌트는 다 api를 가짐. 그래서 웹서비스를 해줌
에이전트 : 얘네는 작업을 하는 일꾼들임.
HTTP 요청이 들어오면 Nova API가 메세지 큐에 던집니다.
그러면 들어온 요청이 물리적인 머신에 돌아야 하니까 Hypervisor가 선택되어야 합니다.
이를 Nova-scheduler가 담당합니다.
그리고 어디에 저장돼야하는지 알려주는 Cinder(볼륨), 하드디스크를 하나 받습니다.
그리고 나서, Nova-Compute 한테 Instance 생성을 요청합니다. (너는 이 정보를 가지고 VM을 만들어줘)
VM의 생성단계
1. Nova-Scheduler에게 Hypervisor 선택
2. Cinder(Volume), Nuetron(Network) 할당 받음
3. Nova-Compute (VM Instance생성)
오픈스택 요청의 최 앞단은 API가 받는거에요. 그래서 분석을 할때도 우리는 API를 먼저 보고, API가 누구한테 보내는지를 타고타고 들어가면서 분석을 할거에요. 우리가 다룰 대부분 코드는 뒷단(하이퍼 바이저 뒤쪽, e.g. 신더볼륨) 을 안봐요. 볼륨을 저장할 데이터는 오래된 백엔드 시스템이에요 (e.g. NFS)
노바컴퓨트는 하이퍼바이저를 뭘 선택하느냐에 따라 다 달라요. 하이퍼바이저는 KVM도 있고 여러가지가 있죠. 생각만해도 머리 아픈 영역이죠.
하지만 API는 웹서비스가 어떻게 동작하는지만 알면 돼요. 우리가 6주까지 볼 영역은 API 그리고 좀더 나아가면 앞단(HTTP)의 클라이언트 영역까지.
IDC에다가 엄청나게 고사양의 컴퓨터를 만들어놔요. 이거(고사양의 컴퓨터)를 하이퍼바이저라 불러요. 하이퍼 바이저 역할은 VM을 띄우게 해주는 서버. (VM을 생성하고 관리함)
인스턴스 만들 때 스펙을 선택했잖아요. vm도 몇코어 짜리. 이런식으로 만들어 질거에요. 이렇게 많은 하이퍼 바이저 중에서, 어디에 새로운 VM을 생성해야할지 결정함. 이러한 결정을 하는게 노바 스케줄러. 어디에 놓을지
노바 스케줄러는 하이퍼 바이저에 대한 정보를 다 수집하고 있음. vm은 몇대, 가용랑 코어는 몇코어, 메모리는 몇기가.
그러면 하이퍼 바이저에는 컴퓨트노드라는 노바컴퓨트가 떠있음. 그럼 사용자는 컴퓨트한테 vm을 만들어줘 라고 함.
보통 많이 쓰이는게 리소스 기반, 가장 여유가 많은 하이퍼 바이저를 선택하게 한다.
더이상 만들 수 없을 때는 오류가 납니다. 운영자는 계속 가용량을 확인을 해야함.
거대하게 큰 NAS를 만들어 놓음.
NAS
Network Attached Storage
LAN으로 연결하는 외장하드
볼륨생성 요청이 들어오면, 해당 크기만큼 파일을 만들고, NFS붙어서 연결을 시켜줌. NAS는 뭐를 하느냐 따라서 구성이 다양해져요. NAS 말고 다른 솔루션 쓸 수도 있고.
아마 AWS 도 이런 구조. 이런 구조를 shared storage 구조라고 많이 불러요.
가상 네트워크를 구성하는 방법. 사용자마다 분리된 환경을 만들어주고 vm들이 붙는거고. 이런 네트워크를 isolated 되어있다고 함.
오픈스택에서 가상 네트워크를 구성하는 방법은 다양함. Vbox는 Vbox 방식대로 가상 네트워크를 붙여줌. 오픈스택은 오픈스위치로 만듬. 깊게 들어가면 어려우니까 이따가 사용해보면서 어떻게 만드는지 위주로 하겠다.
파란색 사용자가 인스턴스를 만들음. 네트워크 노드의 파란색은 공유기의 집합이라고 보면 됨.
오픈스택의 기본 옵션은. a-b 바로 못보내고, 공유기까지 갔다가 가요. 네트워크 노드가 죽으면 오픈스택의 모든 네트워크가 다 죽는다. 기본 흐름은 이런 방식으로 통신흐름. 뉴트론이 이렇게 만들어줌.
오픈스택을 구성하려면 물리적인 것들도 준비해줘야함 API와 에이전트, 2가지로 나눠지는데
API성 서비스 모아 놓는곳 => Controller Node (DB, 메시지큐, API 서비스를 여기에 다 설치함)
컴퓨트 노드 = 하이퍼 바이저, 노바 컴퓨트를 실행시킴
블록스토리지 = 신더, 큰 NAS를 서비스 하는 node들
네트워크 노드
총 4가지로 분류함. 간단하게 봐도 물리장비가 4개 필요한데. VM 1개 에서 가능하게 하는 데브스택.
5가지가 어떻게 동작하는지를 쓰면서 해봅니다.
1.웹 콘솔을 이용하는 방법 2.api를 이용하는 방법 3.cli를 이용하는 방법
1,3번은 결국 openstack api를 호출하게 됩니다. 우리는 그동안 웹 콘솔을 이용해서 오픈스택을 이용했고, 이제 openstack 이라는 명령어를 통해 간단하게 인스턴스 목록을 조회해봅니다. 먼저 웹 콘솔에서 로그인했던 것 처럼 cli에서 인증할 수 있는 정보를 환경변수에 저장해야합니다.
devstack 폴더를 보시면은. openrc 라는 파일이 있어요. 이제 여러분들이 공부해야하는 내용들이 여기서부터 나옵니다.
$pwd
/opt/stack
stack@devstack-master:~$ ls
bin data glance logs nova sandbox
bindep-venv devstack horizon neutron placement tempest
cinder devstack.subunit keystone noVNC requirements
stack@devstack-master:~$ cd devstack/
stack@devstack-master:~/devstack$ ls | grep openrc
openrc
stack@devstack-master:~/devstack$ file openrc
openrc: Bourne-Again shell script, ASCII text executable
stack@devstack-master:~/devstack$ cat openrc | more
#!/usr/bin/env bash
#
# source openrc [username] [projectname]
#
# Configure a set of credentials for $PROJECT/$USERNAME:
# Set OS_PROJECT_NAME to override the default project 'demo'
# Set OS_USERNAME to override the default user name 'demo'
# Set ADMIN_PASSWORD to set the password for 'admin' and 'demo'
# NOTE: support for the old NOVA_* novaclient environment variables has
# been removed.
stack@devstack-master:~/devstack$ source openrc demo
WARNING: setting legacy OS_TENANT_NAME to support cli tools.
stack@devstack-master:~/devstack$ source openrc admin
WARNING: setting legacy OS_TENANT_NAME to support cli tools.
//$source openrc admin
//$source openrc demo
// 를 통해 인증정보를 환경변수에 저장해줍니다.
이제 명령어를 통해 오픈스택의 CLI로 빠집니다.
stack@devstack-master:~/devstack$ openstack
(openstack)
여기서 server list 하면 우리가 아까 대쉬보드에서 만든 서버가 보여요.
stack@devstack-ussuri:~/devstack$ openstack server list
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
| baad8346-1541-4f12-a7f3-a41681398679 | test | ACTIVE | private=fd82:98dc:7575:0:f816:3eff:fec7:61d8, 10.0.0.22, 172.24.4.3 | | m1.nano |
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
근데 얘는 , 어떻게 정보를 가지고 오느냐? 오픈스택 클라이언트(openstackclient)라는 패키지가 설치되어있고, Keystone한테 가서 Token을 얻어오고 이후에 Nova-API로 이 정도를 얻어온다.
이 과정은 Debug Option을 넣음으로써 눈으로 확인 할 수 있다. openstack server list --debug
stack@devstack-master:~/devstack$ openstack server list --debug
stack@devstack-master:~/devstack$ whereis openstack
openstack: /etc/openstack /usr/local/bin/openstack
stack@devstack-master:~/devstack$ which openstack
/usr/local/bin/openstack
명령어의 위치를 확인하였습니다. 내용을 확인해봅니다.
stack@devstack-ussuri:~/devstack$ cat /usr/local/bin/openstack
#!/usr/bin/python3.6
# -*- coding: utf-8 -*-
import re
import sys
from openstackclient.shell import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
stack@devstack-ussuri:~/devstack$
파일을 열어보니 openstackclient 라는 패키지에서 main 함수를 실행하는 것이 전부입니다. 그럼 아까 우리가 실행시켰던 openstack server list가 저 패키지 안에 있다는 뜻입니다.
저 패키지가 저장된 경로는 다음과 같이 확인할 수 있습니다.
stack@devstack-ussuri:~/devstack$ python3
Python 3.6.9 (default, Jul 17 2020, 12:50:27)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import openstackclient
>>> openstackclient
<module 'openstackclient' from '/usr/local/lib/python3.6/dist-packages/openstackclient/__init__.py'>
이제 우리가 시도해볼 것 입니다. openstack server list라는 명령어를 실행했을 때, openstack이라는 명령어 부터 ~ 화면에 출력되어 반환하는 과정까지 어떤 파일들을 건드리는지 Tracing 하면 됩니다.
처음 하시는 분들이 있을 수도 있으니 물고기를 잡는 방법을 알려드리겠습니다.
먼저 단어들로 소스코드 전체를 뒤지세요.가장 잘 찾을 수 있는 방법이에요.
이 코드를 건드릴 거 같다? => print문 삽입.
지름길로 갈게요. 컴퓨트에 서버가 있어요
grep "def list(" -R
recursive 하게.
우리는 이걸로 컨트리뷰션 할 거에요.
꼭 source openrc admin 을 해준 뒤, openstack을 실행시켜야 실행이 된다.
$sudo su- stack
$pwd
/opt/stack
stack@devstack-master:~$ cd devstack/
stack@devstack-master:~/devstack$ source openrc admin
WARNING: setting legacy OS_TENANT_NAME to support cli tools.
stack@devstack-ussuri:~/devstack$ openstack server list
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
| baad8346-1541-4f12-a7f3-a41681398679 | test | ACTIVE | private=fd82:98dc:7575:0:f816:3eff:fec7:61d8, 10.0.0.22, 172.24.4.3 | | m1.nano |
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
VSCode Remote - SSH 를 사용하기 전에는 아래와 같이 Tracing 하였습니다.
stack@devstack-master:~/devstack$ whereis openstack
openstack: /etc/openstack /usr/local/bin/openstack
stack@devstack-ussuri:~/devstack$ cat /usr/local/bin/openstack
#!/usr/bin/python3.6
# -*- coding: utf-8 -*-
import re
import sys
from openstackclient.shell import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
stack@devstack-ussuri:~/devstack$ python3
>>> import openstackclient
>>> openstackclient
<module 'openstackclient' from '/usr/local/lib/python3.6/dist-packages/openstackclient/__init__.py'>
stack@devstack-ussuri:~/devstack$ cat /usr/local/lib/python3.6/dist-packages/openstackclient/shell.py
1.whereis 로 Python file을 찾고 2.cat 으로 file 내용을 보고 3.import 한 module의 경로를 직접 찾았음 눈물없이 볼 수 없는 광경
이제는 VSCode를 사용하여 Tracing 하도록 합니다. Tracing을 위해 자주 사용한 단축키는 아래와 같습니다.
F12 : 함수가 선언된 곳으로 이동
Ctrl + E (or Ctrl+P) : Python File 명으로 검색
Ctrl + Shift + F : 전체 디렉토리에서 검색
Alt + ← : 이전 결과로 돌아가기
Alt + → : 다음 결과로 다시가기
F9 : Break Point
F5 : Debug Mode
F11 : Step into
F10 : Step
stack@devstack-ussuri:~/devstack$ openstack server list
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
| baad8346-1541-4f12-a7f3-a41681398679 | test | ACTIVE | private=fd82:98dc:7575:0:f816:3eff:fec7:61d8, 10.0.0.22, 172.24.4.3 | | m1.nano |
+--------------------------------------+------+--------+---------------------------------------------------------------------+-------+---------+
openstack 이라는 명령어와 Argument는 어떻게 넘어가서 출력되는지 찾으러 떠납니다.
stack@devstack-master:~/devstack$ whereis openstack
openstack: /etc/openstack /usr/local/bin/openstack
Ctrl + Shift + F 를 사용하여 openstack 명령어가 실행된 파일을 클릭합니다.
#!/usr/bin/python3.6
# -*- coding: utf-8 -*-
import re
import sys
from openstackclient.shell import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
line 1 : argument를 받습니다. 출력과 무관할 것이라 예상됩니다.
line 2 : exit(main())으로 어딘가의 main으로 빠집니다. main() 위에 커서를 두고 F12를 클릭합니다
def main(argv=None):
if argv is None:
argv = sys.argv[1:]
if six.PY2:
# Emulate Py3, decode argv into Unicode based on locale so that
# commands always see arguments as text instead of binary data
encoding = locale.getpreferredencoding()
if encoding:
argv = map(lambda arg: arg.decode(encoding), argv)
return OpenStackShell().run(argv)
return 값으로 무엇인가를 넘겨주는 것을 볼 수 있습니다. 이 구문으로부터 출력이 나올것이라 예상하고 run이라는 function 위에서 F12를 눌러줍니다.
class OpenStackShell(app.App):
def run(self, argv):
ret_val = 1
self.command_options = argv
try:
ret_val = super(OpenStackShell, self).run(argv)
return ret_val
except Exception as e:
if not logging.getLogger('').handlers:
logging.basicConfig()
if self.dump_stack_trace:
self.log.error(traceback.format_exc())
else:
self.log.error('Exception raised: ' + str(e))
return ret_val
finally:
self.log.info("END return value: %s", ret_val)
run 이라는 function은 try~except 구문을 사용하고 있습니다.
이 구문을 자세히 보면 return ret_val을 넘겨주는 것을 알 수 있습니다.
따라서, ret_val에 전달되는 값인 super(OpenStackShell, self).run(argv)의 run 함수에 커서를 두고 F12를 누릅니다.
class App(object):
def run(self, argv):
"""
Equivalent to the main program for the application.
:param argv: input arguments and options
:paramtype argv: list of str
"""
try:
self.options, remainder = self.parser.parse_known_args(argv)
self.configure_logging()
self.interactive_mode = not remainder
if self.deferred_help and self.options.deferred_help and remainder:
# When help is requested and `remainder` has any values disable
# `deferred_help` and instead allow the help subcommand to
# handle the request during run_subcommand(). This turns
# "app foo bar --help" into "app help foo bar". However, when
# `remainder` is empty use print_help_if_requested() to allow
# for an early exit.
# Disabling `deferred_help` here also ensures that
# print_help_if_requested will not fire if called by a subclass
# during its initialize_app().
self.options.deferred_help = False
remainder.insert(0, "help")
self.initialize_app(remainder)
self.print_help_if_requested()
except Exception as err:
if hasattr(self, 'options'):
debug = self.options.debug
else:
debug = True
if debug:
self.LOG.exception(err)
raise
else:
self.LOG.error(err)
return 1
result = 1
if self.interactive_mode:
result = self.interact()
else:
result = self.run_subcommand(remainder)
return result
try ~ except 구문을 사용하고 있습니다. 눈여겨 봐야할 것은 어떤 값이 반환되는지보는 return 값 입니다.
return result를 사용하고 있으며, result에는 if ~ else 구문으로 각기 다른 값이 들어가고 있습니다.
remainder is empty use print_help_if_requested() to allow
이 문장에서, arguments가 empty일 때 remainder가 0이 되고 -> interactive_mode=True가 됩니다.
따라서, run_subcommand()로 진입합니다.
class App(object):
def run_subcommand(self, argv):
try:
subcommand = self.command_manager.find_command(argv)
except ValueError as err:
# If there was no exact match, try to find a fuzzy match
the_cmd = argv[0]
fuzzy_matches = self.get_fuzzy_matches(the_cmd)
if fuzzy_matches:
article = 'a'
if self.NAME[0] in 'aeiou':
article = 'an'
self.stdout.write('%s: \'%s\' is not %s %s command. ''See \'%s --help\'.\n'% (self.NAME, ' '.join(argv), article,self.NAME, self.NAME))
self.stdout.write('Did you mean one of these?\n')
for match in fuzzy_matches:
self.stdout.write(' %s\n' % match)
else:
if self.options.debug:
raise
else:
self.LOG.error(err)
return 2
cmd_factory, cmd_name, sub_argv = subcommand
kwargs = {}
if 'cmd_name' in utils.getargspec(cmd_factory.__init__).args:
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self, self.options, **kwargs)
result = 1
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name if self.interactive_mode else ' '.join([self.NAME, cmd_name]))
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
except Exception as err:
if self.options.debug:
self.LOG.exception(err)
else:
self.LOG.error(err)
try:
self.clean_up(cmd, result, err)
except Exception as err2:
if self.options.debug:
self.LOG.exception(err2)
else:
self.LOG.error('Could not clean up: %s', err2)
if self.options.debug:
# 'raise' here gets caught and does not exit like we want
return result
else:
try:
self.clean_up(cmd, result, None)
except Exception as err3:
if self.options.debug:
self.LOG.exception(err3)
else:
self.LOG.error('Could not clean up: %s', err3)
return result
코드가 기니까 중요하지 않은 부분은 설명과 함께 주석으로 처리해서 핵심만 바라봅시다.
try:
subcommand = self.command_manager.find_command(argv)
# Exception 발생에 대한 처리입니다. 넘기도록 합니다.
# except ValueError as err:
# return 2
cmd_factory, cmd_name, sub_argv = subcommand
kwargs = {}
if 'cmd_name' in utils.getargspec(cmd_factory.__init__).args: ①
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self, self.options, **kwargs)
result = 1
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name
if self.interactive_mode
else ' '.join([self.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
# Exception 발생에 대한 처리입니다. 넘기도록 합니다.
# except Exception as err:
# return result
else: ②
try:
self.clean_up(cmd, result, None)
# Exception 발생에 대한 처리입니다. 넘기도록 합니다.
except Exception as err3:
return result
가장 먼저 맨 마지막의 return result를 바라봅니다. 함수를 추적하는데 있어서 제일 먼저 바라봐야 할 것은 함수의 Entry와 Exit 입니다.
result의 값이 유의미하게 쓰여지는 곳은 result = cmd.run(parsed_args) 입니다.
느낌상 cmd.run이 print 해줄 것 같습니다. 일단 따라갑니다.
class Command(command.Command, metaclass=CommandMeta):
def run(self, parsed_args):
self.log.debug('run(%s)', parsed_args)
return super(Command, self).run(parsed_args)
여기서부터는 답을 찾지 못했는데, 아시는 분은 알려주시면 감사할 것 같습니다.
20.08.30
F12를 눌르면 아래의 경로로 갑니다.
class Command(command.Command, metaclass=CommandMeta):
def run(self, parsed_args):
"""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
그러나 실제로 디버깅 진입시에는 아래와 같은 경로로 진입합니다.
@six.add_metaclass(abc.ABCMeta)
class DisplayCommandBase(command.Command):
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
참조한 Site
* https://stackoverflow.com/questions/4799401/pythons-super-abstract-base-classes-and-notimplementederror
* https://docs.python.org/ko/3/library/abc.html
* https://dojang.io/mod/page/view.php?id=2389
* https://www.geeksforgeeks.org/abstract-classes-in-python/
* https://alphahackerhan.tistory.com/34
[Python에서 자식 클래스 확인]
Foo.__subclasses__()
[<class '__main__.Bar'>, <class '__main__.Baz'>]
[Python에서 부모 클래스 확인]
cls.__bases__
(<class '__main__.A'>, <class '__main__.B'>)
[Python에서 Instance 확인]
simclass = Csimple()
isinstance(simclass,Csimple)
# simclass가 Csimple 클래스인지 확인. 결과는 True
마저 진행해보면
@six.add_metaclass(abc.ABCMeta)
class DisplayCommandBase(command.Command):
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() 함수를 통해 출력이 됨을 확인할 수 있습니다.
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
| 6f91eb30-8687-49d7-859f-7284887d57f8 | 1st_Ins | ACTIVE | private=10.0.0.53, fd30:b09a:8b83:0:f816:3eff:fe79:1bec, 172.24.4.59 | | m1.nano |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
함수명에 output 이 있는걸로 보아 출력 된다는 것을 예상 할 수 있지만, 표준출력함수(stdout)이 나올 때 까지 좀 더 Tracing 해보도록 합니다.
@six.add_metaclass(abc.ABCMeta)
class DisplayCommandBase(command.Command):
def run(self, parsed_args):
self.produce_output(parsed_args, column_names, data)
return 0
@abc.abstractmethod
def produce_output(self, parsed_args, column_names, data):
"""Use the formatter to generate the output.
:param parsed_args: argparse.Namespace instance with argument values
:param column_names: sequence of strings containing names of output columns
:param data: iterable with values matching the column names
"""
DisplayCommandBase Class의 produce_output() function 에는 내용이 없습니다.
F11을 누르면 상속받은 Class의 produce_output() function으로 갑니다.
DisplayCommandBase.__subclasses__()
[<class 'cliff.lister.Lister'>, <class 'cliff.lister.Lister'>, <class 'cliff.show.ShowOne'>]
class variables
0:<class 'cliff.lister.Lister'>
1:<class 'cliff.lister.Lister'>
2:<class 'cliff.show.ShowOne'>
@six.add_metaclass(abc.ABCMeta)
class Lister(display.DisplayCommandBase):
def produce_output(self, parsed_args, column_names, data):
if parsed_args.sort_columns and self.need_sort_by_cliff:
indexes = [column_names.index(c) for c in parsed_args.sort_columns if c in column_names]
if indexes:
data = sorted(data, key=operator.itemgetter(*indexes))
(columns_to_include, selector) = self._generate_columns_and_selector(
parsed_args, column_names)
if selector:
# Generator expression to only return the parts of a row
# of data that the user has expressed interest in
# seeing. We have to convert the compress() output to a
# list so the table formatter can ask for its length.
data = (list(self._compress_iterable(row, selector)) for row in data)
self.formatter.emit_list(columns_to_include,data,self.app.stdout,parsed_args,)
return 0
self.formatter.emit_list 이후 출력이 됩니다.
F11을 눌러 emit_list 함수를 따라갑니다.
F12를 눌러 선언을 확인했을 때는 이동하지 않았었습니다. ㅠㅠ.
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
| 6f91eb30-8687-49d7-859f-7284887d57f8 | 1st_Ins | ACTIVE | private=10.0.0.53, fd30:b09a:8b83:0:f816:3eff:fe79:1bec, 172.24.4.59 | | m1.nano |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
class TableFormatter(base.ListFormatter, base.SingleFormatter):
def emit_list(self, column_names, data, stdout, parsed_args):
x = prettytable.PrettyTable(column_names,print_empty=parsed_args.print_empty,)
x.padding_width = 1
# Add rows if data is provided
if data:
self.add_rows(x, column_names, data)
# Choose a reasonable min_width to better handle many columns on a
# narrow console. The table will overflow the console width in
# preference to wrapping columns smaller than 8 characters.
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
stdou.wrtie(formatted) 이후 출력이 나옵니다.
이를 통해서 formatted 라는 곳에 문자열(string)이 들어가 있음을 알 수 있겠습니다.
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
| ID | Name | Status | Networks | Image | Flavor |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
| 6f91eb30-8687-49d7-859f-7284887d57f8 | 1st_Ins | ACTIVE | private=10.0.0.53, fd30:b09a:8b83:0:f816:3eff:fe79:1bec, 172.24.4.59 | | m1.nano |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+
이제 출력이 되는 것을 확인하였으니 변경을 시도해 봅니다.
class TableFormatter(base.ListFormatter, base.SingleFormatter):
def emit_list(self, column_names, data, stdout, parsed_args):
x = prettytable.PrettyTable( column_names, print_empty=parsed_args.print_empty, )
x.padding_width = 1
# Add rows if data is provided if data: self.add_rows(x, column_names, data)
# Choose a reasonable min_width to better handle many columns on a
# narrow console. The table will overflow the console width in
# preference to wrapping columns smaller than 8 characters.
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
x=prettytable.PrettyTable() 에서 +---+ 가 들어가게 이쁘게 테이블을 생성해주는 것 같습니다.
이 때 들어가는 column들은 column_names 라는 argument가 될 것 같네요. 한번 찍어봅시다.
self.add_rows 에서 Data를 추가해줄 것 같네요. F11을 눌러 들어가 봅니다
class TableFormatter(base.ListFormatter, base.SingleFormatter):
def add_rows(self, table, column_names, data):
# Figure out the types of the columns in the
# first row and set the alignment of the
# output accordingly.
data_iter = iter(data)
try:
first_row = next(data_iter)
except StopIteration:
pass
else:
for value, name in zip(first_row, column_names):
alignment = self.ALIGNMENTS.get(type(value), 'l')
table.align[name] = alignment
# Now iterate over the data and add the rows.
table.add_row(_format_row(first_row))
for row in data_iter:
table.add_row(_format_row(row))
fisrt_row 라는 곳에 저희가 원하는 데이터가 들어가 있습니다.
first_row
('6f91eb30-8687-49d7-8...84887d57f8', '1st_Ins', 'ACTIVE', 'private=10.0.0.53, f...72.24.4.59', '', 'm1.nano')
special variables
function variables
0:'6f91eb30-8687-49d7-859f-7284887d57f8'
1:'1st_Ins'
2:'ACTIVE'
3:'private=10.0.0.53, fd30:b09a:8b83:0:f816:3eff:fe79:1bec, 172.24.4.59'
4:''
5:'m1.nano'
len():6
그리고
type(first_row)
<class 'tuple'>
타입은 Tuple이군요 저는 여기서 fisrt_row와 column_names에 item을 추가해서 출력시켜 보겠습니다.
first_row += ('Test:)',)
first_row
('6f91eb30-8687-49d7-8...84887d57f8', '1st_Ins', 'ACTIVE', 'private=10.0.0.53, f...72.24.4.59', '', 'm1.nano', 'Test:)')
column_names += ("TestL:)",)
column_names
('ID', 'Name', 'Status', 'Networks', 'Image', 'Flavor', 'TestL:)')
일단 출력 되었습니다 !
이제 이런 주먹구구식 말고, 좀 더 general 하게 접근할 수 있는 방법을 찾아보겠습니다.
최종적으로 stdout.write 될 때, 사용 되는 것이 column_names 였기 때문입니다. 따라서 Display.py 파일부터 tracing Entry로 잡았습니다.
@six.add_metaclass(abc.ABCMeta)
class DisplayCommandBase(command.Command):
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
=> 표시된 Break point를 걸고 F11로 진입하였습니다.
class ListServer(command.Lister):
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity
image_client = self.app.client_manager.image
project_id = None
# None 이어서 실행되지 않는 문장 제거
## 명령어 입력시 --long을 해야함. 아래 참고한 페이지 서술
if parsed_args.long:
else:
if: parsed_args.no_name_lookup:
else:
columns = (
'ID',
'Name',
'Status',
'Networks',
'Image Name',
'Flavor Name',
)
column_headers = (
'ID',
'Name',
'Status',
'Networks',
'Image',
'Flavor',
)
mixed_case_fields = []
marker_id = None
=> data = compute_client.servers.list(search_opts=search_opts,marker=marker_id,limit=parsed_args.limit)
중간에 data=compute_client.servers.list() 를 담는 곳이 있습니다.
저희가 입력한 명령어는 server list 이기 때문에, 이부분이 실제로 list 자료를 get 하는 부분이 아닐까 의심합니다.
=> 표시된 Break point를 걸고 F11로 진입하였습니다.
[Debug Console로 확인한 parsed_args 의 값들]
parsed_args
all_projects:False
changes_before:None
changes_since:None
columns:[]
deleted:False
fit_width:False
flavor:None
formatter:'table'
no_name_lookup:False
noindent:False
print_empty:False
project:None
project_domain:None
quote_mode:'nonnumeric'
reservation_id:None
sort_columns:[]
status:None
unlocked:False
user:None
user_domain:None
class ServerManager(base.BootingManagerWithFind):
def list(self, detailed=True, search_opts=None, marker=None, limit=None,sort_keys=None, sort_dirs=None):
"""
Get a list of servers.
:param detailed: Whether to return detailed server info (optional).
:param search_opts: Search options to filter out servers which don't
match the search_opts (optional). The search opts format is a
dictionary of key / value pairs that will be appended to the query
string. For a complete list of keys see:
https://docs.openstack.org/api-ref/compute/#list-servers
:param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional).
:param limit: Maximum number of servers to return (optional).
Note the API server has a configurable default limit.
If no limit is specified here or limit is larger than
default, the default limit will be used.
If limit == -1, all servers will be returned.
:param sort_keys: List of sort keys
:param sort_dirs: List of sort directions
:rtype: list of :class:`Server`
Examples:
client.servers.list() - returns detailed list of servers
client.servers.list(search_opts={'status': 'ERROR'}) -
returns list of servers in error state.
client.servers.list(limit=10) - returns only 10 servers
"""
if search_opts is None:
search_opts = {}
....
=> servers = self._list("/servers%s%s" % (detail, query_string),"servers")
result.extend(servers)
result.append_request_ids(servers.request_ids)
return result
함수 내용이 길지만, 주석에 대놓고 "Get a list of server" 라고 써져 있습니다.
servers=self._list를 통해서 서버 명이 전달됩니다
result
[<Server: 1st_Ins>]
0:<Server: 1st_Ins>
special variables
function variables
HUMAN_ID:True
NAME_ATTR:'name'
OS-DCF:diskConfig:'AUTO'
OS-EXT-AZ:availability_zone:'nova'
OS-EXT-STS:power_state:1
OS-EXT-STS:task_state:None
OS-EXT-STS:vm_state:'active'
OS-SRV-USG:launched_at:'2020-08-06T13:21:45.000000'
OS-SRV-USG:terminated_at:None
accessIPv4:''
accessIPv6:''
addresses:{'private': [{...}, {...}, {...}]}
api_version:<APIVersion: 2.1>
config_drive:''
created:'2020-08-06T13:21:28Z'
flavor:{'id': '42', 'links': [{...}]}
hostId:'f0b0f1375254bb6ee4a83c7f4e2ad6331d94f497bb3a9b1848af817a'
human_id:'1st_ins'
id:'6f91eb30-8687-49d7-859f-7284887d57f8'
image:''
key_name:'keyy'
links:[{'href': 'http://192.168.1.10/...84887d57f8', 'rel': 'self'}, {'href': 'http://192.168.1.10/...84887d57f8', 'rel': 'bookmark'}]
manager:<novaclient.v2.servers.ServerManager object at 0x7f65b323eeb8>
metadata:{}
name:'1st_Ins'
networks:OrderedDict([('private', ['10.0.0.53', 'fd30:b09a:8b83:0:f81...:fe79:1bec', '172.24.4.59'])])
os-extended-volumes:volumes_attached:[{'id': '05e026ea-96ed-4499-9...ba1cb63897'}]
progress:0
request_ids:[]
security_groups:[{'name': 'default'}]
status:'ACTIVE'
tenant_id:'4e2abe72f99d484a82473b851a792f8a'
updated:'2020-08-06T13:21:46Z'
user_id:'b1f15ecfa456447c8d69ffaca15a2c69'
x_openstack_request_ids:[]
_add_details:<bound method Resource._add_details of <Server: 1st_Ins>>
_append_request_id:<bound method RequestIdMixin._append_request_id of <Server: 1st_Ins>>
_info:{'OS-DCF:diskConfig': 'AUTO', 'OS-EXT-AZ:availability_zone': 'nova', 'OS-EXT-STS:power_state': 1, 'OS-EXT-STS:task_state': None, 'OS-EXT-STS:vm_state': 'active', 'OS-SRV-USG:launched_at': '2020-08-06T13:21:45.000000', 'OS-SRV-USG:terminated_at': None, 'accessIPv4': '', 'accessIPv6': '', 'addresses': {'private': [...]}, 'config_drive': '', 'created': '2020-08-06T13:21:28Z', 'flavor': {'id': '42', 'links': [...]}, 'hostId': 'f0b0f1375254bb6ee4a8...1848af817a', ...}
_loaded:True
len():1
쫙 긁혀서 나옵니다.
그리고 _info를 보시면 위의 것이 한번 더 정리되어서 아래와 같습니다.
'hostId':'f0b0f1375254bb6ee4a83c7f4e2ad6331d94f497bb3a9b1848af817a'
'image':''
'flavor':{'id': '42', 'links': [{...}]}
'created':'2020-08-06T13:21:28Z'
'updated':'2020-08-06T13:21:46Z'
'addresses':{'private': [{...}, {...}, {...}]}
'accessIPv4':''
'accessIPv6':''
'links':[{'href': 'http://192.168.1.10/...84887d57f8', 'rel': 'self'}, {'href': 'http://192.168.1.10/...84887d57f8', 'rel': 'bookmark'}]
'OS-DCF:diskConfig':'AUTO'
'progress':0
'OS-EXT-AZ:availability_zone':'nova'
'config_drive':''
다시 돌아와서 이후 코드를 진행합니다.
class ListServer(command.Lister):
def take_action(self, parsed_args):
data = compute_client.servers.list(search_opts=search_opts,marker=marker_id,limit=parsed_args.limit)
#있는 list를 싹 다 긁어옴
# 싹다 긁어온거랑, server 에서 가져온거랑 matching 시킴
table = (column_headers,
(utils.get_item_properties(
s, columns,
mixed_case_fields=mixed_case_fields,
formatters={'OS-EXT-STS:power_state': _format_servers_list_power_state,
'Networks': _format_servers_list_networks,
'Metadata': utils.format_dict,
},
) for s in data))
return table
table
0:('ID', 'Name', 'Status', 'Networks', 'Image', 'Flavor')
1:<generator object ListServer.take_action.<locals>.<genexpr> at 0x7f65b2e3c1a8>
len():2
이 Table이 반환되면서 column_names와 Data가 반환되게 되고
우리의 여정은 끝입니다.
오픈스택 팀 멘토이신 최영락(Ian Y. Choi) 님께서 멘토링 해주신 내용을 다룹니다. 오픈스택 팀 멘토이신 조성수 님께서 멘토링 해주신 내용을 다룹니다.
오픈스택에 컨트리뷰션 하기 위해 어떤 환경들이 있는지 알아봅니다
먼저 많은 개발자들이 아시다시피 오픈소스에 기여하는 형상관리로 Github가 있습니다.
오픈스택은 Github에 PR하는 것과는 조금 다른 환경이 구축되어있습니다.
오픈스택은 Github가 아닌 Openstack 전용 Repository를 만들었습니다.
이 Repository는 git.openstack.org 입니다.
이 Repository는 규모가 커져서 지금은 opendev.org 로 OSF
(Openstack Foundation)의 더 많은 Open Source Project들을 다루는 공간으로 변모하였습니다.
환경은 크게 4가지로 구성됩니다. * 소스코드 커밋 리뷰요청 Tool : Gerrit (git 기반) * 문서작성 : rst format ( github에서 markdown과 같은 기능을 합니다) * Bug 리포트 : Launchpad / Storyboard * 소통(메신저/커뮤니케이션) : IRC를 사용합니다 (slack 같은 느낌)
오늘은 Openstack이 어떻게 이루어 있는지 알아보고 컨트리뷰톤을 시작하기 위한 가입 준비를 다루겠습니다.
4 open 이란 ? : Openstack Foundation 에서 강조하는 가치. 4가지 주제로 활동한다.
OpenStack Governance: The Four Opens
Open Source : 우리는 Enterprise Version 안한다. Community 기반이 핵심 가치이다.
Open Design : Design 하는 Process를 공개하겠다.
Open Development : 모두 다 개발에 참여할 수 있다.
Open Community : 특정 회사/국가가 주도하지 않는다. 느린 합의를 지향한다.
Openstack Foundation은 이제 openstack만 하지 않아요. Opensource 기반의 주변 에코시스템을 아우르는, Infra Structure를 만드는 방향성으로 가고있어요.
오픈스택 5가지 메인 프로젝트 : 배포/컨테이너/IaaS/Edge-Computing/CI-CD
OpenStack Governance : 전 세계의 수많은 개발자들이 모이기 때문에
Committees * Foundation Board Director : 이사진의 역할. 기술적으로 뛰어나거나, 오픈스택의 방향성을 결정함. 지원을 하고, 커뮤니티의 투표를 통해서 결정됨. ↔ Openstack Staff : OSF에 돈을 받고 고용된 사람. (행사 준비/운영 진행) * Technical Commitee : Group. 기술위원회. Openstack에 필요한 기능들을 결정하는 사람. 이 또한 지원을 하고 투표를 통해 결정 됨 * User Commitee : 유저 커뮤니티가 잘 운영되도록, 더 많은 사람들이 커뮤니티에 들어올 수 있도록 도와주는 역할
Roles (역할) * Active Technical Contributor(ATC) : 오픈스택의 Release주기(6개월) 이내에 한번이라도 Commit 한 사람. * Active Project Contributor(APC) : ATC보다 발전된 개념. 특정 Project에 많은 공헌을 한 사람. Maintainer의 수준 * Project Team Lead(PTL) : 팀장님. 6개월마다 바뀌긴 하는데 보통 연임함. PTL에 따라서 Project가 움직임. * Core Reviewer : APC의 역할이라고도 볼 수 있는데, Gerrit에 +2점을 줄 수 있음 (Merge 가능) * Active User Contributor(AUC) : 기술적인 것 말고, 유저 커뮤니티를 위해 활동하는 사람
IRC를 기본으로 채팅 함 (전통적인 기술 IRC) :: 인스턴스 식 * 프로젝트 별로 채널을 만들어 놓음 (Slack 같은 느낌) * 문서들이 산발적으로 펼쳐져 있음 * 처음 접할 때 History 파악은 어렵지만, 메인테이너들에게 물어보면서 알게 됨
메일링리스트(ML) - 주소를 구독하는 사람에게 뿌림 :: 중요한 공지/논의 Wiki Page : 정리 Etherpads : 회의록, 아이디어 를 기록하는 곳. 나는 이런 것을 하고 있어 * 오프라인에서 만나서 이야기를 하면서, 이더패드에 정리함 * 대부분의 자료는 이더패드에 있음 * 단점은 이더패드에서 검색이 잘 안됨 * 기능의 History를 찾기 어려움
공식문서의 튜토리얼은 여기 를 참고하시면 됩니다.
이 튜토리얼을 따라하겠습니다. 1. 계정생성 계정 생성은 두 가지를 해야 합니다. * OSF 계정 * StoryBoard 계정 (Ubuntu One 계정) : 코드관리, Bug tracking software
1-1. OSF 계정 https://www.openstack.org/join
위의 페이지에 접속한다
Foundation member 클릭
1-2. StoryBoard 계정 : bug tracking https://storyboard.openstack.org/
위의 페이지에 접속한다
Ubuntu One User로 가입
오픈스택 팀 멘토이신 최영락(Ian Y. Choi) 님께서 멘토링 해주신 내용을 다룹니다.
오픈스택에 컨트리뷰션 하기 위해 어떤 환경들이 있는지 알아봅니다
먼저 많은 개발자들이 아시다시피 오픈소스에 기여하는 형상관리로 Github가 있습니다.
오픈스택은 Github에 PR하는 것과는 조금 다른 환경이 구축되어있습니다.
오픈스택은 Github가 아닌 Openstack 전용 Repository를 만들었습니다.
이 Repository는 git.openstack.org 입니다.
이 Repository는 규모가 커져서 지금은 opendev.org로 되었습니다.
(Openstack Foundation)의 더 많은 Open Source Project들을 다루는 공간으로 변모하였습니다.
환경은 크게 4가지로 구성됩니다. * 소스코드 커밋 리뷰요청 Tool : Gerrit (git 기반) * 문서작성 : rst format ( github에서 markdown과 같은 기능을 합니다) * Bug 리포트 : Launchpad / Storyboard * 소통(메신저/커뮤니케이션) : IRC를 사용합니다 (slack 같은 느낌)
Opensatck의 형상관리방법을 배우기에 앞서서 많은 사람들에게 친숙한 github의 동작원리를 먼저 파악하고, openstack에서는 어떤 점이 다른지 보겠습니다.
형상관리를 모르시는 분들을 위해 git 과 github의 차이를 간단히 설명 드리겠습니다.
Git과 Github 에서 사용되는 용어
Git과 Github의 개념과 용어를 학습하셨습니다. 다음으로 Openstack를 위한 github 사용방법을 배워보겠습니다. * add >> commit >> PR 은 기존의 github사용법입니다. * 오픈스택에서는 add >> commit >> review를 하는 점이 다릅니다.
$ git clone https://github.com/lkeonwoo94/contributhon-2020.gitCloning into 'contributhon-2020'...
$ cd contributhon-2020/
$ git remote -v
origin https://github.com/lkeonwoo94/contributhon-2020.git (fetch)
origin https://github.com/lkeonwoo94/contributhon-2020.git (push)
여기에 upstream을 추가할 수도 있습니다.
upstream 이란? : fork 뜨기 전 main 저장소(Merge되는 원본)를 바라봄
clone한 자신의 repository에서 master branch가 아닌 다른 작업을 하는 공간(branch)를 만듭니다.
$ git branch
* master
$ git checkout -b test
Switched to a new branch 'test'
$ git status
On branch test
nothing to commit, working tree clean
$ git branch
master
* test
branch에서 작업을 마치고 나면 add를 하여 local의 작업공간(clone하고 branch한 곳)에 코드변경사항을 추가하고 commit 을 합니다.
$ git commit -a
이 때, Commit Message 작성 규칙이 있습니다. Openstack에서는 아래와 같은 규칙을 따라야 온전하게 Commit 될 수 있습니다. Opensatck 공식사이트 에서도 찾아볼 수 있습니다.
Git Commit Message 규칙입니다.
Line # |
Name |
Description |
line 1 |
Summary line |
Patch(패치)내용 요약, 50자이내작성 |
line 2 |
공백 |
한줄 띄움 |
line 3 |
Body |
가로 72자 이내 작성 |
line n |
공백 |
한줄 띄움 |
line n+1 |
Tags |
Blue print 내용 : 새로운 기능을 제안 - 제안서 * Implements : 제안 * Partial-Implements : 일부 제안 |
예시는 아래와 같다.
Switch libvirt get_cpu_info method over to use config APIs # Summary Line
The get_cpu_info method in the libvirt driver currently uses # Body line
XPath queries to extract information from the capabilities # Do not over 72
XML document. Switch this over to use the new config class
LibvirtConfigCaps. Also provide a test case to validate
the data being returned.
DocImpact
Closes-Bug: #1003373 # Tag
Implements: blueprint libvirt-xml-cpu-model
Change-Id: I4946a16d27f712ae2adf8441ce78e6c0bb0bb657 # Auto generated
PR이 아니고 Review
를 합니다.
Review
를 이용하여 Gerrit(Openstack의 review system)에 올립니다.
Gerrit을 사용한 git review의 원리를 알아봅니다. 먼저 github 동작 원리 입니다.
gerrit의 동작 원리 입니다.
Openstack 공식 문서에 나와있는 gerrit의 동작 원리 입니다.
Gerrit 을 들어가보겠습니다.
Login을 하면 아까 만들었던 Ubuntu One 의 계정이 필요합니다.
그리고 계약서에 Sign을 하게 됩니다.
Login 후 Settings에서 Individual 을 클릭합니다.
컨트리뷰션한 코드에 대한 저작권을 OSF에 귀속한다는 것에 동의해야 합니다.
위 내용 진행을 위해서 , 이 페이지 를 참고 할 수도 있어요.
그리고 나면, 위 페이지 대로 SSH Key를 등록해야 합니다.
Key가 없을경우 아래처럼 만들 수 있습니다.
ssh-keygen
(인프라) opendev.org 를 직접 만들어서 제공합니다.
git-review install
sudo pip install git-review
Clone Opendev
Fork 없이 Direct로 Clone 합니다.
$ git clone https://opendev.org/opendev/sandbox.git sandbox
$ cd sandbox/
Local의 git을 configuration 해줍니다
git remote -v
# 하면 opendev의 origin만 바라보고있습니다.
# 근데 우리는 gerrit을 연동시켜야하죠
git config --global user.name <username>
git config --global user.email <useremail>
# Gerrit에 연동할 꺼니까, review.opendev에 사용했던
# Ubuntu One 에 등록한 e-mail을 반드시 사용하셔야 합니다.
git config --global gitreview.username <username>
# review.opendec 에 있는 username을 똑같이 맞춰 주셔야합니다
# 안맞추면 push가 안됩니다
$ git review -s
# git review라는 Tool이 Opendev에 내 설정을 넣어줍니다
# 커밋에 gerrit을 통해 아이디가 붙고 원하는 것을 patch(패치)함
실습한 URI https://launchpad.net/openstack-dev-sandbox
유사한 버그가 있는지 검색합니다.
유사한 버그가 없다면
Summary를 적고 Bug 내용을 적고 Commit을 합니다
Bug가 생성됩니다. Bug 번호가 생성됩니다.
Bug 번호가 굉장히 중요해요
버그를 수정하는 Commit을 만들어 볼거에요
vim [파일명 아무거나] [내용도 아무거나 채움]
git add [파일명]
git commit [파일명]
자 여기서부터 아까 배웠던 Commit Message를 적용해주면 됩니다
Buf fix summary line
어쩌구 저쩌구
블라블라블라
Closes-Bug: #[Bugid]
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your barnch is ahead of 'origin/master' by 1 commit.
# *use "git push" to publish your local commits)
#
# Changes to be committed:
# new file : [파일명]
저장해주고 닫으면
git log
깃 리뷰가 알아서 체인지 아이디를 만들어줌 저는 아무짓도 안했는데 Change-ID가 생성이 됐어요 Git Review Tool이, 제가 Commit을 하는 순간 Change-ID를 만들어 줍니다.
git review
#push 대신 review를 사용합니다
이렇게 뜨면 성공이에요
remote 주소로 들어갑니다 : https://review.opendev.org/#/c/750048/
Zuul이 체크하고 Patch를 했어요
Patch Set 2: Verified+1 Build succeeded (check pipeline).
Verified +1은 Zuul이 +1점 준거에요. 아무런 TC가 없어서 Success가 떨어진거에요
그럼 이제 올린 파일을 클릭하고
Line을 클릭하면 적는 공간이 생겨요 여기에 Review 할 Message를 작성하고 Save 해주세요
Comment를 달고나서 위로 올라가기를 클릭해주세요
그리고 Reply를 눌러서 내용을 작성하고 Post를 누르세요. Post를 눌러야 Comment가 달립니다.
자 그럼 이제 CodeReview 받은 내용을 토대로 파일을 다시 수정합니다.
vim [파일명 아까꺼]
# 파일수정
git add [파일명]
git commit --amend
이 상태에서 커밋 메시지를 수정합니다.
그리고 나서
git status
를 확인해보면
Commit 할 것이 없다고 나옵니다. Amend를 하면 수정한걸로 돼요
이 상태에서 다시 review를 올려요. 새로운 review가 만들어 진게 아니고, 아까 만들어놓은 review와 연결이 될 거에요
git review
그리고 다시 그 주소로 들어가보면 PatchSet이 추가가 되었어요
그리고 이렇게 코드 변경점을 확인할 수 있어요 그리고나서 아까 Reply에서 +2점을 줍니다. 그러면 Zuul이 Verified를 돌아서 +1점을 줍니다.
아직 하나더 남았는데요 Workflow+1를 해야합니다. 이것은 PR과 동일합니다. 지금은 저희가 하지만, 실제로는 Maintainer들이 해요
Workflow+1를 하면, Zuul이 다시한번 전체적으로 Verified를 돌고 Merge가 됩니다
Zuul Check : Unit Test
Zuul gate : Integration Test, python version 별 test
Openstack의 번역 모든 번역은 Zanata라는 도구를 쓰고 여기 에서 합니다.
별도로 가입을 해주셔야 합니다.
그리고 Explorer Tab에서 아래와 같이 번역작업을 진행할 수 있어요
위 Site에서 Login이 되지 않는 오류가 있습니다.
이렇게 Login을 시도하면 에러가 뜹니다.
주소창을 보니 Create_user로 되어있어요. 이상하네요
그래서 제가 찾은 방법은 다음과 같습니다
Sign up 에서 아무렇게 입력을 해줍니다.
그리고 sign up을 누르면
로그인 할 수 있는 페이지로 바뀝니다.
이상태에서 Log in 을 누르면
Login이 됩니다... 이상하네요
일전에 Python기반의 Project들은 RST(Restructed Text)를 사용한다고 한 적이 있습니다.
오늘은 RST 문서 작성법에 대해 알아보려 합니다.
How to contribute on this repository
Markdown의 ReadMe.md와 비슷하다고 보시면 됩니다.
아래와 같이 작성해줍니다.
※ 참고 : https://github.com/openstack-kr/contributhon-2020/blob/master/doc/source/contributors/index.rst
작성
.. _contributors: # 책갈피 용도로 사용한다. _[폴더명] 정도면 괜찮을 듯 하다.
============================== # 제목 위 아래에 = 으로 표시한다. 길이가 제목과 같거나 길어야 한다.
참고: 컨트리뷰션을 위한 가이드
============================== # 위에 제목을 잘 적고, 해당 줄은 윗 부분 = 길이와 동일하게 한다.
.. toctree:: # 목차를 위한 문법 (그대로 사용하자)
:maxdepth: 1 # 1단계 목차로 끝난다는 것을 의미 (그대로 사용하자)
how_to_contribute.rst # 실제 rst 문법으로 적을 문서 파일명으로 대체
# 여러 파일로 나누고자 할 경우에는 해당 파일명도 추가하자.
결과
아시겠죠? 위 경우는 Depth가 1이지만(하위폴더가 없음), 만약 하위 폴더가 있는 경우 아래와 같이 작성합니다.
.. toctree:: # 최상위 문서 기준 목차 (그대로 사용하자)
:maxdepth: 2 # 하위폴더를 포함한 최대 Depth
openstack_helm/index # 폴더의 예시. 폴더/파일 이므로 depth가 2입니다
[만든 폴더]/index # 새로 만든 폴더 내 index를 가리키도록 추가하자.
제목은 == 로 감싸며,
부제목은 아래만 -
소제목은 아래에 ~ 을 사용
링크는 링크할 말
이미지는 .. image:: 경로.png
주의 : 각 행은 최대 80자만 허용한다는 기본 규칙
예시
===============
제목이 들어갑니다
===============
부제목이들어갑니다
----------------
소제목이들어갑니다
~~~~~~~~~~~~~~~~
`링크를할거에요 <http://~~~~>`_
# 할거에요 < <-- 꺽쇠 들어가기 전에 한칸 띄워줘야 해요
# `_ <- 이후에 한칸 띄워주셔야 해요
이미지를 넣을거에요
.. image:: folder/image_file.png
# image:: <- 한칸 띄워주셔야 해요
*기울임글씨체에요*
코드블럭을 넣을거에요
.. code-block:: none
<- "엔터를 넣어주셔야 해요"
어쩌구저쩌구
인덴트를 넣어주세요
여기까지 코드블럭이에요
* bullet을 넣어줄 거에요
RST 문서를 작성하고나면 Local에서 Test를 합니다. Python으로 Build하며, tox라는 프로그램을 사용합니다.
tox 설치
sudo pip install tox
문서 빌드
$ tox -e py38 docs
# py38 자리에는 사용할 Python version을 넣을 수 있습니다.
문서 build가 되면 doc/build/html 폴더에 index.html 폴더를 열어서 확인할 수 있습니다.
$ open doc/build/html/index.html
Commit Message를 잘 적어준다. 이전 번에 배웠습니다 ! 참고 할 수 있어요 링크 를 참고할 수도 있습니다.
예시
Switch libvirt get_cpu_info method over to use config APIs # Summary Line
The get_cpu_info method in the libvirt driver currently uses # Body line
XPath queries to extract information from the capabilities # Do not over 72
XML document. Switch this over to use the new config class
LibvirtConfigCaps. Also provide a test case to validate
the data being returned.
DocImpact
Closes-Bug: #1003373 # Tag
Implements: blueprint libvirt-xml-cpu-model
Change-Id: I4946a16d27f712ae2adf8441ce78e6c0bb0bb657 # Auto generated
PR Message를 아래와 같이 적어준다.
## WHY (PR을 제출하는 이유)
본 저장소에 컨트리뷰톤 활동 결과 정리를 위한 구체적인 가이드 내용을 문서로 정리합니다.
## HOW (PR을 통해 어떤 부분을 수정하고자 하는가)
RST 및 본 저장소에 작성하는 형식에 따라 추가합니다.
## Related Issues
#이슈번호
## Checklist (확인사항)
Checking the releveant checkboxes(`[x]`) 확인사항으로는 다음 3개가 있습니다
CLA 서명여부 : Apache License 2.0에 동의합니다.
Issue 연결 : 본인이 진행하는 이슈와 연관하고, 없으면 이슈를 생성합니다. Openstack에서 Closes-Bug # 번호와 같습니다.
리뷰어의 내용을 반영할 의지가 있는지 동의 여부입니다.
CLA 서명 여부를 확인합니다.
deploy-preview를 하며 PR을 기준으로 Build한 결과가 나옵니다.
Build에 실패하면, Log를 보고 실패 이유를 참고하여 수정합니다.
올린 PR을 추가로 수정하고 싶은 경우 기존 Commit을 --amend 하고 --force push를 합니다. * --force push를 하지 않으면 git push가 되지 않습니다. * 그러한 이유(reject이 되는 이유)는 Commit은 기본적으로 누적이 되어야 하는데, 누적을 하지 않으므로 push를 허용하지 않는 것 입니다. Commit을 깔끔하게 관리 할 수 있느나, 대신 이전의 Commit 내용을 확인 할 수 없다는 단점 또한 동시에 지니고 있습니다.
launchpad id : keonwoolee
zanata id : keonwoo_lee
stackanalytics pr : stackalytics/default_data#213
OpenStack 프로젝트의 i18n을 위해 구현 된 번역 관리 시스템이다. OpenStack의 공식 문서들을 각 국가의 언어들로 번역 / 리뷰할 수 있다. OpenStack id를 통해 쉽게 가입이 가능하다. 또한 stackanalytics를 통해 확인 가능하다.
DevStack is a series of extensible scripts used to quickly bring up a complete OpenStack environment based on the latest versions of everything from git master. It is used interactively as a development environment and as the basis for much of the OpenStack project’s functional testing.
문서에서 설명한 그대로 DevStack은 git 기반으로 OpenStack 실행 환경을 빠르게 구현 할 수 있다. 실제 릴리즈된 버전에서 버그를 재현하고, 수정 및 기여하기 위해서는 DevStack으로 OpenStack 실행 환경을 구현해 놓아야 한다.
공식 문서 에서 설치하는 과정을 잘 설명해 놓았다. 특정 구간에서의 문제만 조심하면 빠르고 쉽게 DevStack을 설치 할 수 있을 것이다.
DevStack은 다음과 같은 운영체제(latest version)를 지원한다.
Ubuntu
CentOS/RHEL 8
OpenSUSE
$ sudo useradd -s /bin/bash -d /opt/stack -m stack
$ echo "stack ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/stack
$ sudo su - stack
$ git clone https://opendev.org/openstack/devstack
$ cd devstack
git clone을 해온 devstack 디렉토리 내에 local.conf를 작성해주도록 하자.
local.conf는 상황에 따라서 적절히 조정해주도록 하자.
ADMIN_PASSWORD=secret
DATABASE_PASSWORD=$ADMIN_PASSWORD
RABBIT_PASSWORD=$ADMIN_PASSWORD
SERVICE_PASSWORD=$ADMIN_PASSWORD
HOST_IP=[접근하는 시스템의 공인IP]
1~4번 과정을 정상적으로 마쳤다면 git 디렉토리 내에서 stack.sh를 실행하면 정상적으로 설치가 진행된다.
./stack.sh #디렉토리 내에서 실행하면 자동으로 설치가 진행된다.
방화벽상에서 OpenStack에서 사용하는 포트를 개방해주어야 한다. firewall-cmd를 통해서 포트 개방을 해주도록 하자.
22(ssh), 80(HTTP), 6080
novaclient에는 서버 태그 기능이 존재하나, openstack client에는 존재하지 않는다는 이슈
기존에 분산되어있던 명령어를 openstack client로 통합하는 과정에서 이전되지 않은 기능들이 존재한다.
nova client의 코드를 분석 후, openstack client에 맞추어 이식해야 할 것으로 추정된다.
openstack image list라는 명령어에서 --name 옵션이 wildcard 문자나 string 검색을 지원하지 않는다라는 이슈이다.
image list에는 cirros, ubuntu, ubuntu-iso-test가 들어있다.
"ubuntu 18.03"을 지정하면 정상적으로 출력된다.
하지만 ubuntu* 같이 wildcard 명령어를 쓸 경우, 정상적으로 출력되지 않는다.
image list를 불러오는 단계에서 --name 옵션에서는 스트링에 대해 직접적으로 비교하는 것으로 추정된다.
비교하는 단계에서 re를 통한 정규표현식 지원 기능을 추가하면 해결될 것으로 보인다.
launchpad id : dohan0930
zanata id : revival0n
stackanalytics pr : https://github.com/stackalytics/default_data/pull/214