[OSS] #OpenStack 실습 (Feat. VSCode 원격 디버깅)

2020. 8. 21. 02:00Cloud

728x90

1. Remote Debugging with VSCode

References

https://github.com/openstack-kr/contributon-2020/blob/master/Remote%20Debugging%20VSCode.md

 

openstack-kr/contributon-2020

Contribute to openstack-kr/contributon-2020 development by creating an account on GitHub.

github.com

먼저 실습을 진행하기 위한 VSCode 환경설정을 진행합니다.

Putty로 OpenStackClient를 Tracing 하기에는 많이 불편한 감이 있었습니다.

 

위의 포스팅을 참조하여 진행했으며, 따라하였으나 원격 접속 진입이 되지 않아 재작성합니다.

 

로컬설정                                                                                                                             

1. VSCode로 들어가서 Remote-SSH를 설치합니다.

 

2. VSCode 좌측 하단에 있는 초록색 버튼을 클릭한 후 Remote-SSH: Connet to Host...를 클릭합니다.

 

3. Configure SSH Host를 클릭합니다

 

4. 아래와 같이 Config file을 작성해줍니다.

Host : VSCode Remote - SSH 에서 보이는 이름 명
HostName : Remote Debugging 할 Target 의 IP Address
User : Remote SSH 접속할 Target의 login ID
IdentityFile : SSH 접속시 필요한 pem(개인키) 파일

* 개인키 파일은 잘 복사해서 .ssh 경로에 붙여넣기 하도록 합니다

 

개인키를 넣어놓는다

 

설정후에 VSCode를 껏다가 다시 켜주시고요,

아래 보이는 Host 명을 클릭하면 접속이 됩니다.

Config File에서 입력한 Host 명이 보인다.

 

좌측 하단의 SSH: 초록색 버튼을 통해 정상 접속이 된것을 알 수 있습니다.

 

저는 참조한 링크처럼 시도를 했을 때 아래와 같은 에러가 발생했습니다.

이 에러에 2박3일 내 눈물을 바친다 


원격지 설정                                                                                                                         

 

root 계정이 아닐 시 Debugging을 할 수 없는 문제가 있었습니다.

Root 계정으로 접속하는 방법입니다. 먼저 일반 계정으로 접속한뒤 root 계정으로 switch user 하여 .ssh 내용을 변경토록 합니다.

# root 계정으로 전환
$ sudo su - root

# .ssh 내용 변경
root@:~# vim ~/.ssh/authorized_keys
#ssh-rsa 앞 까지 주석을 지워주면 됨

 

Local의 Remote - SSH Config File도 수정해줍니다.

 

이후 접속하여, 아래와 같이 설정해줍니다.

1. Open Folder를 클릭하고 Path를 입력합니다. 저는 Search로 찾을 거라 대충 / 경로로 넣었습니다.

어짜피 터미널 입력도 가능합니다

[Python Extension 설치]

$ /usr/bin/python3.6 -m pip install --upgrade pip
$ /usr/bin/python3.6 /home/ubuntu/.vscode-server/extensions/ms-python.python-2020.8.101144/pythonFiles/pyvsc-run-isolated.py pip install -U pylint --user

 

[Debugg launch.json 설정]

Python file을 하나 연뒤 F5 (Debug)를 하게 되면 Add Configuration을 통해 만들 수 있습니다.

생성된 파일에 아래와 같이 설정해줍니다.

/.vscode 에 생성된다. 최상위 디렉토리

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    
        "version": "0.2.0",
        "configurations": [
            

            {
                "name": "Python: Current File",
                "type": "python",
                "request": "launch",
                
                // 이곳은 시작할 프로그램 명을 넣습니다.
                "program": "/usr/local/bin/openstack",
                
                // 이곳은 arguments들을 입력합니다
                "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"
}

2. [실습] Openstack server list가 출력되는 과정 Tracing 하기

 

꼭  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

1.출발 : openstack 명령어

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/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())
  • line 1 : argument를 받습니다. 출력과 무관할 것이라 예상됩니다.
  • line 2 : exit(main())으로 어딘가의 main으로 빠집니다. main() 위에 커서를 두고 F12를 클릭합니다

/usr/local/lib/python3.6/dist-packages/openstackclient/shell.py
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를 눌러줍니다.

/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)
              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를 누릅니다.
Super : Python의 상속의 개념에서 나오며, 자식클래스가 부모 클래스를 사용할 경우 사용합니다.
class className(Parent) : 괄호 안에 들어있는 것은 상속받는 부모 입니다.

/usr/local/lib/python3.6/dist-packages/cliff/app.py
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()로 진입합니다.

/usr/local/lib/python3.6/dist-packages/cliff/app.py
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 해줄 것 같습니다. 일단 따라갑니다.

/usr/local/lib/python3.6/dist-packages/osc_lib/command/command.py
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를 눌르면 아래의 경로로 갑니다.

/usr/local/lib/python3.6/dist-packages/osc_lib/command/command.py
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

 

그러나 실제로 디버깅 진입시에는 아래와 같은 경로로 진입합니다.

/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

 

참조한 Site

 

Python's super(), abstract base classes, and NotImplementedError

Abstract base classes can still be handy in Python. In writing an abstract base class where I want every subclass to have, say, a spam() method, I want to write something like this: class Abstract(

stackoverflow.com

 

abc — 추상 베이스 클래스 — Python 3.8.5 문서

abc — 추상 베이스 클래스 소스 코드: Lib/abc.py 이 모듈은, PEP 3119에서 설명된 대로, 파이썬에서 추상 베이스 클래스 (ABC) 를 정의하기 위한 기반 구조를 제공합니다; 이것이 왜 파이썬에 추가되었�

docs.python.org

 

파이썬 코딩 도장: 36.6 추상 클래스 사용하기

파이썬은 추상 클래스(abstract class)라는 기능을 제공합니다. 추상 클래스는 메서드의 목록만 가진 클래스이며 상속받는 클래스에서 메서드 구현을 강제하기 위해 사용합니다. 먼저 추상 클래스��

dojang.io

 

Abstract Classes in Python - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

파이썬 메타클래스 쉽고 깊게 이해하기, Python Metaclass A to Z

Prerequisite 파이썬 메타클래스를 이해하기전에 먼저 확실하게 구분하고 가야하는 것이 있는데, "객체와 인스턴스의 차이"이다. 우리가 객체 지향 언어를 배울 때, 대개 클래스라는 '틀'부터 객체��

alphahackerhan.tistory.com


[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

마저 진행해보면

/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
  • 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 해보도록 합니다.

/usr/local/lib/python3.6/dist-packages/cliff/display.py
 @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'>

/usr/local/lib/python3.6/dist-packages/cliff/lister.py
@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 |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+

/usr/local/lib/python3.6/dist-packages/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
  • 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 |
+--------------------------------------+---------+--------+----------------------------------------------------------------------+-------+---------+

이제 출력이 되는 것을 확인하였으니 변경을 시도해 봅니다.


3. [실습] Openstack server list Table 변경하여 출력하기

 

/usr/local/lib/python3.6/dist-packages/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
  • x=prettytable.PrettyTable() 에서 +---+ 가 들어가게 이쁘게 테이블을 생성해주는 것 같습니다.
  • 이 때 들어가는 column들은 column_names 라는 argument가 될 것 같네요. 한번 찍어봅시다.
#DEBUG CONSOLE
column_names
('ID', 'Name', 'Status', 'Networks', 'Image', 'Flavor')
  • self.add_rows 에서 Data를 추가해줄 것 같네요. F11을 눌러 들어가 봅니다

/usr/local/lib/python3.6/dist-packages/cliff/formatters/table.py
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:)')

 

/usr/local/lib/python3.6/dist-packages/cliff/display.py
column_names += ("TestL:)",)

column_names
('ID', 'Name', 'Status', 'Networks', 'Image', 'Flavor', 'TestL:)')

 

  • 일단 출력 되었습니다 !
  • 이제 이런 주먹구구식 말고, 좀 더 general 하게 접근할 수 있는 방법을 찾아보겠습니다.

1. 가장 먼저 추측해본 곳은 column_names에 정보가 담기는 곳입니다.

최종적으로 stdout.write 될 때, 사용 되는 것이 column_names 였기 때문입니다.

따라서 Display.py 파일부터 tracing Entry로 잡았습니다.

/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
        
  • => 표시된 Break point를 걸고 F11로 진입하였습니다.
/usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py
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

/usr/local/lib/python3.6/dist-packages/novaclient/v2/servers.py
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':''

다시 돌아와서 이후 코드를 진행합니다.

/usr/local/lib/python3.6/dist-packages/openstackclient/compute/v2/server.py
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가 반환되게 되고
  • 우리의 여정은 끝입니다.

 

■ References

 

server list 명령어 알아보기 - 오픈스택(Openstack)

주의) 생각의 흐름대로 분석을 해보았습니다. 보다 보면 블랙홀(?)로 들어가는 경우도 있고, 중간에 몰라서 추측으로 넘기는 곳도 많으니 참고해주시고 읽어주시면 감사하겠습니다. 1. 시작하기

epicarts.tistory.com

 

openstack 명령어 분석하기

이번주는 오픈스택을 설치하고 openstackclient 코드 분석하는 시간을 가졌습니다.

medium.com