Inventory 플러그인 개발

이 문서는 SpaceONE의 마이크로서비스 중 하나인 Inventory의 플러그인을 개발하는 방법을 안내합니다. 이 예시는 나의 자산을 관리하고 추적해주는 Portfolio 플러그인을 사용하여 가격 기준 상위 5개의 암호화폐의 정보를 수집하는 방법을 보여줍니다.

1. 환경 구성

❗️
플러그인 개발을 진행하기 전에 반드시 SpaceONE 설치를 먼저 진행해주세요!

플러그인 개발에 앞서 특정 환경 구성이 필요합니다. 필요한 라이브러리와 지원 버전 정보는 아래 표를 참고하세요.

이름버전 정보
spaceone-inventory2.0.dev210

2. 환경 설정

1) 패키지 설치


sudo pip3 install spaceone-inventory --pre --upgrade

2) 디렉터리 생성


mkdir plugin-portfolio-inven-collector && cd $_

3) Inventory 플러그인 프로젝트 생성


spaceone create-project src/plugin -s asset

src/plugin 디렉터리에는 아래와 같은 파일이 생성됩니다.

        • __init__.py
        • main.py
  • 3. main.py 파일 구성

    1) collector_init 함수

    ℹ️
    collector_init 함수는 플러그인을 초기화하는 함수입니다. 이 함수는 플러그인이 시작될 때 호출됩니다.

    plugin-portfolio-inven-collector 예제의 경우 collector_init 함수에서 options_schema 가 필요하지 않으므로 아래와 같이 작성합니다.

    src/plugin/main.py
    from spaceone.inventory.plugin.collector.lib.server import CollectorPluginServer
    
    app = CollectorPluginServer()
    
    
    @app.route("Collector.init")
    def collector_init(params: dict) -> dict:
        """init plugin by options
    
        Args:
            params (CollectorInitRequest): {
                'options': 'dict',    # Required
                'domain_id': 'str'
            }
    
        Returns:
            PluginResponse: {
                'metadata': 'dict'
            }
        """
        return {"metadata": {"options_schema": {}}}

    2) collector_collect 함수

    ℹ️
    collector_collect 함수를 작성하기 전에 비즈니스 로직을 처리할 manager 패키지를 만들어봅시다.

    mkdir -p src/plugin/manager touch src/plugin/manager/__init__.py touch src/plugin/manager/cryptocurrency_manager.py

    2-1) 위 명령어들의 결과로 생성되는 디렉터리/파일들의 구조는 아래와 같습니다.

          • __init__.py
          • cryptocurrency_manager.py
        • __init__.py
        • main.py
  • 2-2) 먼저, 아래와 같이 스켈레톤 코드를 작성해 주세요.

    src/plugin/manager/cryptocurrency_manager.py
    import logging
    
    from spaceone.core.manager import BaseManager
    
    _LOGGER = logging.getLogger(__name__)
    
    
    class CryptoManager(BaseManager):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
        def collect_resources(self, options, secret_data, schema):
            pass

    2-3) 다시 main.py 로 돌아와 collector_collect 함수를 작성합니다.

    src/plugin/main.py
    from typing import Generator
    
    from spaceone.inventory.plugin.collector.lib.server import CollectorPluginServer
    
    from plugin.manager.crypto_manager import CryptoManager
    
    ...
    
    @app.route("Collector.collect")
    def collector_collect(params: dict) -> Generator[dict, None, None]:
        ...
        
        options = params["options"]
        secret_data = params["secret_data"]
        schema = params.get("schema")
    
        crypto_mgr = CryptoManager()
        return crypto_mgr.collect_resources(options, secret_data, schema)

    4. 비즈니스 로직 작성 및 외부 연결

    이제 암호화폐들의 정보를 수집하는 비즈니스 로직을 작성해봅시다.

    1) Manager

    1-1) __init__

    src/plugin/manager/cryptocurrency_manager.py
    import logging
    import os
    
    from spaceone.core.manager import BaseManager
    
    _LOGGER = logging.getLogger(__name__)
    _CURRENT_DIR = os.path.dirname(__file__)
    _METADATA_DIR = os.path.join(_CURRENT_DIR, "../metadata/")
    
    
    class CryptoManager(BaseManager):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.provider = "portfolio"
            self.cloud_service_group = "Investment"
            self.cloud_service_type = "Cryptocurrency"
            self.metadata_path = os.path.join(
                _METADATA_DIR, "investment/cryptocurrency.yaml"
            )
        ...

    provider > cloud_service_group > cloud_service_type

    • provider는 AWS, Azure, Google Cloud와 같은 클라우드 서비스 제공자를 의미합니다.
      • 여기서는 간단한 설명을 위해 ‘portfolio’로 설정합니다.
    • cloud_service_group은 provider의 하위 개념으로 왼쪽 사이드바의 제일 상단에 위치합니다.
    • cloud_service_type은 클라우드 서비스 그룹의 하위 개념으로, 클라우드 서비스 그룹 하위에 여러 타입이 존재할 수 있습니다.
    • metadata_path는 클라우드 서비스 타입의 메타데이터를 저장하는 경로를 의미합니다.
      • 이 메타데이터를 이용하여 클라우드 서비스 타입을 조금 더 풍부하게 꾸밀 수 있습니다.

    1-2) collect_resources

    아래와 같이 스켈레톤 코드를 작성합니다.

    src/plugin/manager/cryptocurrency_manager.py
    import logging
    
    from spaceone.core.manager import BaseManager
    from spaceone.inventory.plugin.collector.lib import (
        make_cloud_service_type,
        make_error_response,
        make_response,
    )
    
    _LOGGER = logging.getLogger(__name__)
    
    
    class CryptoManager(BaseManager):
        ...
         
        def collect_resources(self, options, secret_data, schema):
            try:
                yield from self.collect_cloud_service_type(options, secret_data, schema)
                yield from self.collect_cloud_service(options, secret_data, schema)
            except Exception as e:
                yield make_error_response(
                    error=e,
                    provider=self.provider,
                    cloud_service_group=self.cloud_service_group,
                    cloud_service_type=self.cloud_service_type,
                )
    
        def collect_cloud_service_type(self, options, secret_data, schema):
            cloud_service_type = make_cloud_service_type(
                name=self.cloud_service_type,
                group=self.cloud_service_group,
                provider=self.provider,
                metadata_path=self.metadata_path,
                is_primary=True,
                is_major=True,
            )
    
            yield make_response(
                cloud_service_type=cloud_service_type,
                match_keys=[["name", "reference.resource_id", "account", "provider"]],
                resource_type="inventory.CloudServiceType",
            )
    
        def collect_cloud_service(self, options, secret_data, schema):
            pass

    collect_cloud_service 메서드를 작성하기 전에 암호화폐 정보를 수집하기 위해 외부와 연결하는 connector 패키지를 생성이 필요합니다.

    2) Connector

    2-1) connector 패키지를 생성합니다.


    mkdir -p src/plugin/connector touch src/plugin/connector/__init__.py touch src/plugin/connector/cryptocurrency_connector.py

    2-2) 위 명령어들의 결과로 생성되는 디렉터리/파일들의 구조는 아래와 같습니다.

          • __init__.py
          • cryptocurrency_connector.py
          • __init__.py
          • cryptocurrency_manager.py
        • __init__.py
        • main.py
  • 2-3) 이제 pycoingecko 라이브러리를 이용하여 암호화폐 중 현재 시점을 기준으로 가장 가격이 높은 5개의 코인 정보(coins)를 가져옵니다.


    pip3 install pycoingecko
    src/plugin/connector/cryptocurrency_connector.py
    import logging
    from typing import Dict, List
    
    from pycoingecko import CoinGeckoAPI
    from spaceone.core.connector import BaseConnector
    
    _LOGGER = logging.getLogger(__name__)
    
    
    class CryptoConnector(BaseConnector):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.client = CoinGeckoAPI()
    
        def list_cryptocurrencies(self) -> List[Dict]:
            try:
                coins = self.client.get_coins_markets(
                    vs_currency="krw", order="market_cap_desc", per_page=5, page=1
                )
    
                filtered_coins = list()
                for coin in coins:
                    filtered_coins.append(
                        {
                            "name": coin["name"],
                            "current_price": coin["current_price"],
                            "market_cap_rank": coin["market_cap_rank"],
                            "price_change_percentage_24h": coin[
                                "price_change_percentage_24h"
                            ],
                            "high_24h": coin["high_24h"],
                            "low_24h": coin["low_24h"],
                            "last_updated": coin["last_updated"],
                        }
                    )
                return filtered_coins
            except Exception as e:
                _LOGGER.error(f"Error fetching cryptocurrency data: {e}")
                return []
    🧐

    위 코드에서 filtered_coins에 포함된 정보는 다음과 같습니다.

    • pycoingecko 라이브러리의 get_coins_markets 메서드로부터 Raw Data를 받아온 후, 이 데이터에서 사용자에게 보여줄 정보만을 선별하여 filtered_coins에 담아 리턴합니다.
    참고로 coins에 들어가는 Raw Data 정보는 다음과 같습니다.
    [
      {
        "id": "bitcoin",
        "symbol": "btc",
        "name": "Bitcoin",
        "image": "https://coin-images.coingecko.com/coins/images/1/large/bitcoin.png?1696501400",
        "current_price": 89860690,
        "market_cap": 1773045681619077,
        "market_cap_rank": 1,
        "fully_diluted_valuation": 1887089127326028,
        "total_volume": 53990231060500,
        "high_24h": 92076398,
        "low_24h": 87861025,
        "price_change_24h": -1924582.0696388185,
        "price_change_percentage_24h": -2.09683,
        "market_cap_change_24h": -36837579495029.25,
        "market_cap_change_percentage_24h": -2.03536,
        "circulating_supply": 19730896.0,
        "total_supply": 21000000.0,
        "max_supply": 21000000.0,
        "ath": 98576718,
        "ath_change_percentage": -8.99866,
        "ath_date": "2024-06-07T13:55:20.414Z",
        "atl": 75594,
        "atl_change_percentage": 118568.64714,
        "atl_date": "2013-07-05T00:00:00.000Z",
        "roi": None,
        "last_updated": "2024-07-25T17:07:53.002Z",
      }
    
      ...
    ]

    2-4) 다시 cryptocurrency_connector의 호출부인 cryptocurrency_manager.py의 collect_cloud_service 메서드로 돌아가서 코드를 완성해봅시다.

    3) Manager

    src/plugin/manager/cryptocurrency_manager.py
    ...
    
    from spaceone.inventory.plugin.collector.lib import (
        make_cloud_service_type,
        make_cloud_service_with_metadata,
        make_error_response,
        make_response,
    )
    
    from plugin.connector.cryptocurrency_connector import CryptoConnector
    
    ...
    
    class CryptoManager(BaseManager):
        ...
        
        def collect_cloud_service(self, options, secret_data, schema):
            crypto_connector = CryptoConnector()
            cryptocurrencies = crypto_connector.list_cryptocurrencies()
    
            for crypto in cryptocurrencies:
                cloud_service = make_cloud_service_with_metadata(
                    name=crypto["name"],
                    cloud_service_type=self.cloud_service_type,
                    cloud_service_group=self.cloud_service_group,
                    provider=self.provider,
                    data=crypto,
                    data_format="dict",
                    metadata_path=self.metadata_path,
                )
                yield make_response(
                    cloud_service=cloud_service,
                    match_keys=[["name", "reference.resource_id", "account", "provider"]],
                )

    5. Metadata YAML 파일 작성

    필터링된 정보들을 사용자에게 제공해주기 위한 yaml 파일을 작성합니다.


    mkdir -p src/plugin/metadata/investment touch src/plugin/metadata/investment/cryptocurrency.yaml
    ⚠️
    yaml 파일의 경로는 src/plugin/metadata/[cloud_service_group]/[cloud_service_type].yaml 의 컨벤션을 가지고 있습니다. 따라서 개발시 이 경로의 컨벤션을 따라야 합니다.

    아래 yaml 파일을 보면 위에서 유저에게 보여주길 원했던 필터링 된 데이터들이 전부 data. 의 형태로 value로 들어가있는 것을 확인할 수 있습니다.

    src/plugin/metadata/investment/cryptocurrency.yaml
    search:
      fields:
        - 이름: data.name
        - 시가총액 순위: data.market_cap_rank
    table:
      sort:
        key: data.market_cap_rank
        desc: true
      fields:
        - 이름: data.name
        - 가격: data.current_price
        - 가격 업데이트 시간 (UTC): data.last_updated
        - 시가총액 순위: data.market_cap_rank
        - 24시간 변동률 (%): data.price_change_percentage_24h
        - 24시간 최저가: data.low_24h
        - 24시간 최고가: data.high_24h
    tabs.0:
      name: Details
      type: item
      fields:
        - 이름: data.name
        - 가격: data.current_price
        - 가격 업데이트 시간 (UTC): data.last_updated
        - 시가총액 순위: data.market_cap_rank
        - 24시간 변동률 (%): data.price_change_percentage_24h
        - 24시간 최저가: data.low_24h
        - 24시간 최고가: data.high_24h

    6. 실행

    플러그인 개발 준비가 완료되었습니다. 이제 spacectl 명령어와 간단한 YAML 파일을 사용하여 테스트를 진행해 보겠습니다.

    1) 플러그인(gRPC) 서버 실행

    PyCharm이나 CLI를 사용하여 서버를 실행할 수 있습니다.

    1-1) PyCharm 이용

    • 먼저 src 디렉터리를 ‘Sources Root’로 설정합니다.

    • Run/Debug Configurations에서 아래와 같이 설정하고 ‘Run’ 버튼을 이용하여 플러그인 서버를 실행합니다.

    1-2) CLI 이용

    • 플러그인 서버 실행 후 결과는 아래와 같습니다.

    spaceone run plugin-server -s src plugin
    2024-07-16T10:48:44.515Z [DEBUG]       (server.py:69) Loaded Services: 
             - spaceone.api.inventory.plugin.Collector
             - spaceone.api.inventory.plugin.Job
             - grpc.health.v1.Health
             - spaceone.api.core.v1.ServerInfo  
    2024-07-16T10:48:44.515Z [INFO]       (server.py:73) Start gRPC Server (plugin): port=50051, max_workers=100

    2) init 테스트

    init.yaml
    ---
    options: { }

    2-1) 플러그인 서버가 실행 중인 상태에서 새로운 CLI 탭을 이용하여 아래 명령어를 입력합니다.


    spacectl exec init plugin.Collector -f init.yaml
    init의 결과는 아래와 같습니다.
    ---
    metadata:
      filter_format: [ ]
      options_schema: { }
      supported_features:
        - garbage_collection
      supported_resource_type:
        - inventory.CloudService
        - inventory.CloudServiceType
        - inventory.Region
        - inventory.Namespace
        - inventory.Metric
      supported_schedules:
        - hours

    3) collect 테스트

    collect.yaml
    ---
    options: { }
    secret_data: { }
    spacectl exec collect plugin.Collector -f collect.yaml

    3-1) 위 명령의 결과로 아래 정보들이 수집되어 나타납니다.

    • Cloud Service Type인 Cryptocurrency
    • Cryptocurrency의 5개 인스턴스
    collect의 결과는 아래와 같습니다.
    ---
    match_rules:
      '1':
      - name
      - reference.resource_id
      - account
      - provider
    resource:
      group: Investment
      is_major: true
      is_primary: true
      json_metadata: '{"view": {"search": [{"name": "이름", "key": "data.name", "type":
        "text"}, {"name": "시가총액 순위", "key": "data.market_cap_rank", "type": "text"}],
        "table": {"layout": {"type": "query-search-table", "options": {"default_sort":
        {"key": "data.market_cap_rank", "desc": true}, "fields": [{"name": "이름", "key":
        "data.name", "type": "text"}, {"name": "가격", "key": "data.current_price", "type":
        "text"}, {"name": "가격 업데이트 시간 (UTC)", "key": "data.last_updated", "type": "text"},
        {"name": "시가총액 순위", "key": "data.market_cap_rank", "type": "text"}, {"name": "24시간
        변동률 (%)", "key": "data.price_change_percentage_24h", "type": "text"}, {"name":
        "24시간 최저가", "key": "data.low_24h", "type": "text"}, {"name": "24시간 최고가", "key":
        "data.high_24h", "type": "text"}]}, "name": "Main Table"}}, "sub_data": {"layouts":
        [{"name": "Details", "type": "item", "options": {"fields": [{"name": "이름", "key":
        "data.name", "type": "text"}, {"name": "가격", "key": "data.current_price", "type":
        "text"}, {"name": "가격 업데이트 시간 (UTC)", "key": "data.last_updated", "type": "text"},
        {"name": "시가총액 순위", "key": "data.market_cap_rank", "type": "text"}, {"name": "24시간
        변동률 (%)", "key": "data.price_change_percentage_24h", "type": "text"}, {"name":
        "24시간 최저가", "key": "data.low_24h", "type": "text"}, {"name": "24시간 최고가", "key":
        "data.high_24h", "type": "text"}]}}]}}}'
      labels: []
      metadata: null
      name: Cryptocurrency
      provider: portfolio
      service_code: null
      tags: {}
    resource_type: inventory.CloudServiceType
    state: SUCCESS
    
    ---
    match_rules:
      '1':
      - name
      - reference.resource_id
      - account
      - provider
    resource:
      account: null
      cloud_service_group: Investment
      cloud_service_type: Cryptocurrency
      data:
        current_price: 89860690.0
        high_24h: 92076398.0
        last_updated: '2024-07-25T17:07:53.002Z'
        low_24h: 87861025.0
        market_cap_rank: 1.0
        name: Bitcoin
        price_change_percentage_24h: -2.09683
      instance_size: 0.0
      instance_type: null
      ip_addresses: []
      json_data: null
      json_metadata: null
      launched_at: null
      metadata:
        view:
          search:
          - key: data.name
            name: 이름
            type: text
          - key: data.market_cap_rank
            name: 시가총액 순위
            type: text
          sub_data:
            layouts:
            - name: Details
              options:
                fields:
                - key: data.name
                  name: 이름
                  type: text
                - key: data.current_price
                  name: 가격
                  type: text
                - key: data.last_updated
                  name: 가격 업데이트 시간 (UTC)
                  type: text
                - key: data.market_cap_rank
                  name: 시가총액 순위
                  type: text
                - key: data.price_change_percentage_24h
                  name: 24시간 변동률 (%)
                  type: text
                - key: data.low_24h
                  name: 24시간 최저가
                  type: text
                - key: data.high_24h
                  name: 24시간 최고가
                  type: text
              type: item
          table:
            layout:
              name: Main Table
              options:
                default_sort:
                  desc: true
                  key: data.market_cap_rank
                fields:
                - key: data.name
                  name: 이름
                  type: text
                - key: data.current_price
                  name: 가격
                  type: text
                - key: data.last_updated
                  name: 가격 업데이트 시간 (UTC)
                  type: text
                - key: data.market_cap_rank
                  name: 시가총액 순위
                  type: text
                - key: data.price_change_percentage_24h
                  name: 24시간 변동률 (%)
                  type: text
                - key: data.low_24h
                  name: 24시간 최저가
                  type: text
                - key: data.high_24h
                  name: 24시간 최고가
                  type: text
              type: query-search-table
      name: Bitcoin
      provider: portfolio
      reference: null
      region_code: null
      tags: {}
    resource_type: inventory.CloudService
    state: SUCCESS
    
    ...

    3-2) Cryptocurrency의 인스턴스 중 하나인 Bitcoin의 정보를 확인해보면 resourcedata 필드에 필터링 된 정보들이 들어가있는 것을 확인할 수 있습니다.

    그리고 metadata에는 이 필터링된 정보를 각 인스턴스의 Detail에서 확인할 수 있습니다.

    이로써 SpaceONE Cryptocurrency 플러그인 개발이 완료되었습니다. 앞서 설명드린 예제를 참고하여 누구나 플러그인 개발을 시작할 수 있습니다.

    개발한 플러그인을 UI를 통해 유저에게 보여주고 싶다면 다음 페이지의 플러그인 등록방법을 확인하세요.