Python ベースの ControlScript を使用する制御プレーンと Lua ベースの DataScript を使用するデータ プレーンの両方にスクリプトを使用することで、広範な動作のカスタマイズと自動化を行うことができます。
DataScript
DataScript は、仮想サービス単位、またはクライアント単位で NSX Advanced Load Balancer の動作をカスタマイズするための強力なメカニズムです。DataScript は、Lua でコーディングされた軽量スクリプトです。これらのスクリプトは、TCP 接続、HTTP 要求と応答、またはデータ プレーン内の他のイベントを行う各クライアントに対して実行できます。
1 つ以上の DataScript を仮想サービスのルール セクションに添付できます。
スクリプトは、[要求イベント スクリプト] セクションまたは [応答イベント スクリプト] セクションにアップロードまたはコピーして貼り付けできます。たとえば、セキュア ディレクトリへのアクセスを制限するために、次のテキストが [要求イベント スクリプト] セクションに貼り付けられます。
if avi.http.uri == "/secure/" then avi.http.send(403) end
DataScript のコマンドとサンプルの完全なドキュメントについては、『VMware NSX Advanced Load Balancer DataScript ガイド』を参照してください。
ControlScript
ControlScript は、NSX Advanced Load Balancer Controller で実行される Python ベースのスクリプトです。これらはアラート アクションによって開始されます。アラート アクション自体は、システム内のイベントによってトリガされます。ControlScript は、特定のイベントが発生したことを管理者に警告するのではなく、NSX Advanced Load Balancer 構成の変更や外部システムへのカスタム メッセージの送信などの特定のアクションを実行できます。たとえば、現在のサーバがリソース キャパシティに達し、健全性スコアの低下が発生している場合は、VMware’s vCenter により多くのサーバをスケール アウトするように指示します。
ControlScript を作成するには、次の手順を実行します。
の順に移動します。
[作成] をクリックします。[新しい ControlScript] 画面が表示されます。
ControlScript の [名前] を入力します。
注:デフォルトでは、[テキストの入力] オプションが選択されています。
指定されたテキスト ボックスにユーザー定義のアラート アクション スクリプトを入力するか、
.py
ファイルをアップロードします。[保存] をクリックします。
ControlScript は、コントローラの Linux サブシステム内で制限された権限で実行されます。ファイル システムへのアクセスは読み取り専用であるため、新しいファイルを作成したり、ファイルやディレクトリを変更したりすることはできません。
例外:ControlScript には、割り当て制限が 10 MB の /tmp
ディレクトリへの読み取りアクセスと書き込みアクセスの両方が許可されます。/tmp
に書き込まれたファイルは一時的なもので、ControlScript が終了するとすぐに失われます。
control-script には、コントローラ システム ファイルおよびコントローラ システム Python ライブラリへのいずれのタイプのアクセスもありません。ControlScript で使用するためにコンテナで共有する必要があるスクリプト ファイルは、/opt/avi/python/lib/avi/scripts/csshared フォルダに配置する必要があります。これは、import avi.scripts.csshared.<module>
コードを使用してインポートできます。
Python コマンドの例と定義については、標準の Python ドキュメントを参照してください。NSX Advanced Load Balancer 構成の変更は、標準の API メカニズムを介して Linux から NSX Advanced Load Balancer への API 呼び出しによって行うことができます。
ControlScript に渡される変数のセットは 2 つあります。以下が含まれます。
環境変数
スクリプトの引数
環境変数
ControlScript 内では、次の環境変数を使用できます。
USER/ API_TOKEN
:NSX Advanced Load Balancer REST API での認証に使用できるユーザー名と API トークンが含まれています。TENANT/ TENANT_UUID
:ControlScript が実行されているテナント コンテキスト(名前/UUID)が含まれています。DOCKER_GATEWAY
:ControlScript が NSX Advanced Load Balancer REST API との通信に使用できるローカル IP アドレスが含まれています。VERSION
:ControlScript が実行されているコントローラのバージョンが含まれています。EVENT_DESCRIPTION
:アラートを起動したイベントの説明が含まれています。
次の例は、リリース 21.1.4 を実行しているコントローラの ControlScript に渡されるこれらの環境変数の内容を示しています。これらは Python で os.environ()
を使用して取得されます。
{ "USER": "admin", "API_TOKEN": "60bc0fc8ece748c4f20f0eabf7a25eb6584af0aa", "TENANT": "admin", "TENANT_UUID": "admin", "DOCKER_GATEWAY": "172.17.0.1", "VERSION": "21.1.4", "EVENT_DESCRIPTION": "Config vs1 update status is success (performed by user admin)", }
NSX Advanced Load Balancer REST API にアクセスするには、DOCKER_GATEWAY 環境変数に渡された IP アドレスを使用する必要があります。
スクリプトの引数
スクリプトに提供される引数は、次の形式の配列として提供されます。
['/home/admin/{{ ALERT NAME }}' , '{{ ALERT DETAILS }}']
アラートの詳細
アラートの詳細は JSON データとして提供され、JSON を使用して json.loads(sys.argv[1])
として解析できます。ControlScript に提供されるデータの美しい印刷 JSON を次に示します。
{ "name": "System-CC-Alert-cluster-878226b4-ff2c-4e6b-a9a7-66aa58ad23f1-1580412633.0-1580412633-38875205", "throttle_count": 0, "level": "ALERT_LOW", "reason": "threshold_exceeded", "obj_name": "AWS", "threshold": 1, "events": [ { "event_id": "AWS_ACCESS_FAILURE", "event_details": { "aws_infra_details": { "vpc_id": "vpc-0617ccd15673817c0", "region": "eu-central-1", "error_string": "AuthFailure: AWS was not able to validate the provided access credentials\\n\\tstatus code: 401, request id: f22641cb-140b-4ff2-ab8d-0008a73c6fbf", "cc_id": "cloud-8a3e601b-d06d-4998-bc41-ac3004330066" } }, "obj_uuid": "cluster-878226b4-ff2c-4e6b-a9a7-66aa58ad23f1", "obj_name": "AWS", "report_timestamp": 1580412633 } ] }
JSON データのサイズが 128kb を超える場合、破棄されます。
例
スクリプトを理解するには、次に示す値の使用方法の例を使用します。
ControlScript に渡されるデータの取得
#!/usr/bin/python # # NSX Advanced Load Balancer ControlScript # # This sample ControlScript will output the environment values, and alert # arguments that are passed from the alert that triggered the alert script. # You can use these values to help construct your python script actions to # handle the alert. # # import os import sys if __name__ == "__main__": print("Environment Vars: %s \n" % os.environ) print("Alert Arguments: %s \n" % sys.argv)
スティッキー プール グループ
#!/usr/bin/python3 import os import sys import json from avi.sdk.avi_api import ApiSession import urllib3 import requests if hasattr(requests.packages.urllib3, 'disable_warnings'): requests.packages.urllib3.disable_warnings() if hasattr(urllib3, 'disable_warnings'): urllib3.disable_warnings() def ParseAviParams(argv): if len(argv) != 2: return alert_params = json.loads(argv[1]) print(str(alert_params)) return alert_params def get_api_token(): return os.environ.get('API_TOKEN') def get_api_user(): return os.environ.get('USER') def get_api_endpoint(): return os.environ.get('DOCKER_GATEWAY') or 'localhost' def get_tenant(): return os.environ.get('TENANT') def failover_pools(session, pool_uuid, pool_name, retries=5): if retries <= 0: return 'Too many retry attempts - aborting!' query = 'refers_to=pool:%s' % pool_uuid pg_result = session.get('poolgroup', params=query) if pg_result.count() == 0: return 'No pool group found referencing pool %s' % pool_name pg_obj = pg_result.json()['results'][0] highest_up_pool = None highest_down_pool = None for member in pg_obj['members']: priority_label = member['priority_label'] member_ref = member['pool_ref'] pool_runtime_url = ('%s/runtime/detail' % member_ref.split('/api/')[1]) pool_obj = session.get(pool_runtime_url).json()[0] if pool_obj['oper_status']['state'] == 'OPER_UP': if (not highest_up_pool or int(highest_up_pool[1]) < int(priority_label)): highest_up_pool = (member, priority_label, pool_obj['name']) elif (not highest_down_pool or int(highest_down_pool[1]) < int(priority_label)): highest_down_pool = (member, priority_label, pool_obj['name']) if not highest_up_pool: return ('No action required as all pools in the ' 'pool group are now down.') elif not highest_down_pool: return ('No action required as all pools in the ' 'pool group are now up.') if int(highest_down_pool[1]) <= int(highest_up_pool[1]): return ('No action required. The highest-priority available ' 'pool (%s) already has a higher priority than the ' 'highest-priority non-available pool (%s)' % (highest_up_pool[2], highest_down_pool[2])) highest_up_pool[0]['priority_label'] = highest_down_pool[1] highest_down_pool[0]['priority_label'] = highest_up_pool[1] p_result = session.put('poolgroup/%s' % pg_obj['uuid'], pg_obj) if p_result.status_code < 300: return ', '.join(['Pool %s priority changed to %s' % (p[0], p[1]) for p in ((highest_up_pool[2], highest_down_pool[1]), (highest_down_pool[2], highest_up_pool[1]))]) if p_result.status_code == 412: return failover_pools(session, pool_uuid, pool_name, retries - 1) return 'Error setting pool priority: %s' % p_result.text if __name__ == "__main__": alert_params = ParseAviParams(sys.argv) events = alert_params.get('events', []) if len(events) > 0: token = get_api_token() user = get_api_user() api_endpoint = get_api_endpoint() tenant = get_tenant() pool_uuid = events[0]['obj_uuid'] pool_name = events[0]['obj_name'] event_id = events[0]['event_id'] try: with ApiSession(api_endpoint, user, token=token, tenant=tenant) as session: result = failover_pools(session, pool_uuid, pool_name) except Exception as e: result = str(e) else: result = 'No event data for ControlScript' print(result) # Use with a ControlScript and Alert(s) to perform 'sticky' failover of pool groups. # # Alert should trigger on 'Pool Up' and 'Pool Down' events. #
GCP SE へのルートの追加
#!/usr/bin/python import sys, os, json, traceback, re, time from avi.sdk.avi_api import ApiSession from oauth2client.client import GoogleCredentials from googleapiclient import discovery ''' This ControlScript is executed on the Controller every time there is a CC_IP_ATTACHED or a CC_IP_DETACHED event. CC_IP_ATTACHED: Event is triggered when a VIP is attached to a SE CC_IP_DETACHED: Event is triggered when a VIP is detached from a SE, usually when a SE goes down or a scale in occurs The goal of this script is to add a route to GCP with the destination as the VIP and nextHopIp as the GCP instance IP on which the SE is running after a CC_IP_ATTACHED event. After a CC_IP_DETACHED event, the goal of the script is to remove the corresponding route. Script assumptions: 1) The Controller GCP instance has scope=compute-rw to be able to modify routes in GCP 2) 'description' field in the Service Engine Group is configured as a JSON encoded string containing GCP project, zone and network Event details contain the SE UUID and the VIP. 1) GET SE object from UUID and extract SE IP address (which is the same as the GCP instance IP address) and Service Engine Group link 2) GET Service Engine Group object. The 'description' field in the Service Engine Group is a JSON encoded string containing GCP project and network URL. Extract project and network from the 'description' field 3) Extract all routes matching destRange as VIP from GCP 4) If event is CC_IP_DETACHED, remove matching route with destRange as vip and nextHopIp as instance IP in the appr network If event is CC_IP_ATTACHED and no matching route exists already, add a new route with destRange as vip and nextHopIp as instance IP in appr network ''' def parse_avi_params(argv): if len(argv) != 2: return {} script_parms = json.loads(argv[1]) return script_parms def create_avi_endpoint(): token=os.environ.get('API_TOKEN') user=os.environ.get('USER') # tenant=os.environ.get('TENANT') return ApiSession.get_session(os.environ.get('DOCKER_GATEWAY'), user, token=token, tenant='admin') def google_compute(): credentials = GoogleCredentials.get_application_default() return discovery.build('compute', 'v1', credentials=credentials) def gcp_program_route(gcp, event_id, project, network, inst_ip, vip): # List all routes for vip result = gcp.routes().list(project=project, filter='destRange eq %s' % vip).execute() if (('items' not in result or len(result['items']) == 0) and event_id == 'CC_IP_DETACHED'): print(('Project %s destRange %s route not found' % (project, vip))) return if event_id == 'CC_IP_DETACHED': # Remove route for vip nextHop instance for r in result['items']: if (r['network'] == network and r['destRange'] == vip and r['nextHopIp'] == inst_ip): result = gcp.routes().delete(project=project, route=r['name']).execute() print(('Route %s delete result %s' % (r['name'], str(result)))) # Wait until done or retries exhausted if 'name' in result: start = int(time.time()) for i in range(0, 20): op_result = gcp.globalOperations().get(project=project, operation=result['name']).execute() print(('op_result %s' % str(op_result))) if op_result['status'] == 'DONE': if 'error' in result: print(('WARNING: Route delete had errors ' 'result %s' % str(op_result))) else: print(('Route delete done result %s' % str(op_result))) break if int(time.time()) - start > 20: print(('WARNING: Wait exhausted last op_result %s' % str(op_result))) break else: time.sleep(1) else: print('WARNING: Unable to obtain name of route delete ' 'operation') elif event_id == 'CC_IP_ATTACHED': # Add routes to instance # Route names can just have - and alphanumeric chars rt_name = re.sub('[./]+', '-', 'route-%s-%s' % (inst_ip, vip)) route = {'name': rt_name, 'destRange': vip, 'network': network, 'nextHopIp': inst_ip} result = gcp.routes().insert(project=project, body=route).execute() print(('Route VIP %s insert result %s' % (vip, str(result)))) def handle_cc_alert(session, gcp, script_parms): se_name = script_parms['obj_name'] print(('Event Se %s %s' % (se_name, str(script_parms)))) if len(script_parms['events']) == 0: print ('WARNING: No events in alert') return # GET SE object from Avi for instance IP address and SE Group link rsp = session.get('serviceengine?uuid=%s' % script_parms['events'][0]['event_details']['cc_ip_details']['se_vm_uuid']) if rsp.status_code in range(200, 299): se = json.loads(rsp.text) if se['count'] == 0 or len(se['results']) == 0: print(('WARNING: SE %s no results' % script_parms['events'][0]['event_details']['cc_ip_details']['se_vm_uuid'])) return inst_ip = next((v['ip']['ip_addr']['addr'] for v in se['results'][0]['mgmt_vnic']['vnic_networks'] if v['ip']['mask'] == 32 and v['mode'] != 'VIP'), '') if not inst_ip: print(('WARNING: Unable to find IP with mask 32 SE %s' % str(se['results'][0]))) return # GET SE Group object for GCP project, zones and network # https://localhost/api/serviceenginegroup/serviceenginegroup-99f78850-4d1f-4b7b-9027-311ad1f8c60e seg_ref_list = se['results'][0]['se_group_ref'].split('/api/') seg_rsp = session.get(seg_ref_list[1]) if seg_rsp.status_code in range(200, 299): vip = '%s/32' % script_parms['events'][0]['event_details']['cc_ip_details']['ip']['addr'] seg = json.loads(seg_rsp.text) descr = json.loads(seg.get('description', '{}')) project = descr.get('project', '') network = descr.get('network', '') if not project or not network: print(('WARNING: Project, Network is required descr %s' % str(descr))) return gcp_program_route(gcp, script_parms['events'][0]['event_id'], project, network, inst_ip, vip) else: print(('WARNING: Unable to retrieve SE Group %s status %d' % (se['results'][0]['se_group_ref'], seg_rsp.status_code))) return else: print(('WARNING: Unable to retrieve SE %s' % script_parms['events'][0]['obj_uuid'])) # Script entry if __name__ == "__main__": script_parms = parse_avi_params(sys.argv) try: admin_session = create_avi_endpoint() gcp = google_compute() handle_cc_alert(admin_session, gcp, script_parms) except Exception: print(('WARNING: Exception with Avi/Gcp route %s' % traceback.format_exc()))
サービス拒否攻撃の処理
#!/usr/bin/python import sys, os, json from avi.sdk.avi_api import ApiSession ''' This control script will be executed in the Avi Controller when an alert due to a DOS_ATTACK event is generated. An example params passed to the control script dos-attack.py is as follows params = [u'/home/admin/Dos_Attack-l4vs', '{"name": "Dos_Attack-virtualservice-d1093604-e1f0-476a-ad91-01c5224c5641-1461261720.83-1461261716-77911185", "throttle_count": 0, "level": "ALERT_HIGH", "reason": "threshold_exceeded", "obj_name": "l4vs", "threshold": 1, "events": [ { "event_id": "DOS_ATTACK", "event_details": { "dos_attack_event_details": { "attack_count": 2150.0, "attack": "SYN_FLOOD", "ipgroup_uuids": [ "ipaddrgroup-f6883289-39fa-418f-94c2-3b8f8093cd7a" ], "src_ips": ["10.10.90.67"] } }, "obj_uuid": "virtualservice-d1093604-e1f0-476a-ad91-01c5224c5641", "obj_name": "l4vs", "report_timestamp": 1461261716 } ] }' ] The DOS_ATTACK event was generated due to a SYN_FLOOD from client 10.10.90.67. It was traffic to the Virtual Service : "l4vs". The offending client ip is added as NETWORK_SECURITY_POLICY_ACTION_TYPE_DENY in the network seurity policy for the virtual service ''' def ParseAviParams(argv): if len(argv) != 2: return alert_dict = json.loads(argv[1]) return alert_dict def create_avi_endpoint(): token=os.environ.get('API_TOKEN') user=os.environ.get('USER') # tenant=os.environ.get('TENANT') return ApiSession.get_session(os.environ.get('DOCKER_GATEWAY'), user, token=token, tenant='admin') def add_ns_rules_dos(session, dos_params): vs_name = dos_params['obj_name'] vs_uuid = '' client_ips = [] vs_name = dos_params['obj_name'] for event in dos_params['events']: vs_uuid = event['obj_uuid'] dos_attack_event_details = event['event_details']['dos_attack_event_details'] if dos_attack_event_details['attack'] != 'SYN_FLOOD': continue for ip in dos_attack_event_details['src_ips']: client_ips.append(ip) if len(client_ips) == 0: print ('DOS ATTACK is not SYN_FLOOD. Ignoring') return print('VS name : ' + vs_name + ' VS UUID : ' + vs_uuid + ' Client IPs : ' + str(client_ips)) ip_list = [] for ip in client_ips: ip_addr_obj = { 'addr' : ip, 'type' : 'V4' } ip_list.append(ip_addr_obj) match_obj = { 'match_criteria' : 'IS_IN', 'addrs' : ip_list } ns_match_target_obj = { 'client_ip' : match_obj } ns_rule_dos_obj = { 'enable' : True, 'log' : True, 'match' : ns_match_target_obj, 'action' : 'NETWORK_SECURITY_POLICY_ACTION_TYPE_DENY' } ns_policy_dos_obj = { 'vs_name' : vs_name, 'vs_uuid' : vs_uuid, 'rules' : [ ns_rule_dos_obj, ] } print('ns_policy_dos_obj : ' + str(ns_policy_dos_obj)) try : session.post(path='networksecuritypolicydos?action=block', data=ns_policy_dos_obj) except Exception as e: print(str(e)) print(('Added Client IPs ' + str(client_ips) + \ ' in the blocked list for VS : ' + vs_name)) if __name__ == "__main__": alert_dict = ParseAviParams(sys.argv) try : admin_session = create_avi_endpoint() except Exception as e: print('login failed to Avi Controller!' + str(e)) sys.exit(0) add_ns_rules_dos(admin_session, alert_dict)