Inventory 플러그인 개발
이 문서는 SpaceONE의 마이크로서비스 중 하나인 Inventory의 플러그인을 개발하는 방법을 안내합니다. 이 예시는 나의 자산을 관리하고 추적해주는 Portfolio 플러그인을 사용하여 가격 기준 상위 5개의 암호화폐의 정보를 수집하는 방법을 보여줍니다.
1. 환경 구성
플러그인 개발에 앞서 특정 환경 구성이 필요합니다. 필요한 라이브러리와 지원 버전 정보는 아래 표를 참고하세요.
이름 | 버전 정보 |
---|---|
spaceone-inventory | 2.0.dev210 |
2. 환경 설정
1) 패키지 설치
2) 디렉터리 생성
3) Inventory 플러그인 프로젝트 생성
src/plugin 디렉터리에는 아래와 같은 파일이 생성됩니다.
- __init__.py
- main.py
3. main.py 파일 구성
1) collector_init
함수
collector_init
함수는 플러그인을 초기화하는 함수입니다. 이 함수는 플러그인이 시작될 때 호출됩니다.plugin-portfolio-inven-collector 예제의 경우 collector_init
함수에서 options_schema
가 필요하지 않으므로 아래와 같이 작성합니다.
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 패키지를 만들어봅시다.2-1) 위 명령어들의 결과로 생성되는 디렉터리/파일들의 구조는 아래와 같습니다.
- __init__.py
- cryptocurrency_manager.py
- __init__.py
- main.py
2-2) 먼저, 아래와 같이 스켈레톤 코드를 작성해 주세요.
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
함수를 작성합니다.
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__
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
아래와 같이 스켈레톤 코드를 작성합니다.
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 패키지를 생성합니다.
2-2) 위 명령어들의 결과로 생성되는 디렉터리/파일들의 구조는 아래와 같습니다.
- __init__.py
- cryptocurrency_connector.py
- __init__.py
- cryptocurrency_manager.py
- __init__.py
- main.py
2-3) 이제 pycoingecko
라이브러리를 이용하여 암호화폐 중 현재 시점을 기준으로 가장 가격이 높은 5개의 코인 정보(coins
)를 가져옵니다.
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
...
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 파일을 작성합니다.
아래 yaml 파일을 보면 위에서 유저에게 보여주길 원했던 필터링 된 데이터들이 전부 data.
의 형태로 value로 들어가있는 것을 확인할 수 있습니다.
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 이용
- 플러그인 서버 실행 후 결과는 아래와 같습니다.
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
테스트
---
options: { }
2-1) 플러그인 서버가 실행 중인 상태에서 새로운 CLI 탭을 이용하여 아래 명령어를 입력합니다.
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
테스트
---
options: { }
secret_data: { }
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
의 정보를 확인해보면 resource
에 data
필드에 필터링 된 정보들이 들어가있는 것을 확인할 수 있습니다.
그리고 metadata
에는 이 필터링된 정보를 각 인스턴스의 Detail에서 확인할 수 있습니다.
이로써 SpaceONE Cryptocurrency 플러그인 개발이 완료되었습니다. 앞서 설명드린 예제를 참고하여 누구나 플러그인 개발을 시작할 수 있습니다.
개발한 플러그인을 UI를 통해 유저에게 보여주고 싶다면 다음 페이지의 플러그인 등록방법을 확인하세요.