2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Serverless で MisocaAPI(OAuth2)を利用するサービスを作るまで on Cloud9

Last updated at Posted at 2017-11-25

はじめに

Serverless フレームワークを用いて Misoca API を使ってみたので手順をまとめます。
開発環境としてCloud9を利用します。Lambda 関数は Python3 で記述します。

Misocaについて実装していますがOAuth2を用いるサービスであれば基本的に同じ感じでできる、、はず?

1. Cloud9のセットアップ

  • cloud9でワークスペースを作ります (テンプレートはBlankを選択しました)

Cloud9でワークスペースを作成

1.1. Python のバージョンを 3.6 にする

[参考] http://imabari.hateblo.jp/entry/2017/10/09/223748

現在のバージョンを確認
$ python -V
Python 2.7.6
aptのリポジトリを追加してupdateしてからpython3.6をインストール
$ sudo add-apt-repository ppa:jonathonf/python-3.6
$ sudo apt-get update
$ sudo apt-get install python3.6
pipを更新
wget https://bootstrap.pypa.io/ez_setup.py
sudo -H python3.6 ez_setup.py
wget https://bootstrap.pypa.io/get-pip.py
sudo -H python3.6 get-pip.py
シンボリックリンクを張り直す
$ sudo rm /usr/bin/python3
$ sudo ln -s /usr/bin/python3.6 /usr/bin/python3
$ sudo rm /usr/bin/python
$ sudo ln -s /usr/bin/python3 /usr/bin/python
pythonとpipのバージョンを確認
$ python -V
Python 3.6.3
$ pip -V
pip 9.0.1 from /usr/local/lib/python3.6/dist-packages (python 3.6)

1.2. AWS CLIのインストール

python3.6-devをインストール
$ sudo apt-get install python3-dev
$ sudo -H pip install awscli   

[未解決] awscliインストール時にPyYAMLのビルドに失敗のエラーメッセージが表示されるが何故かインストールは成功する、、、

1.3. AWS CLIの設定

AWSのIAMから KeyとSecretを取得しておきます
[参考] http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_access-keys.html

awscliの初期設定
$ aws configure
AWS Access Key ID [None]: [Your Key Here]
AWS Secret Access Key [None]: [Your Secret Here]
Default region name [None]: ap-northeast-1
Default output format [None]: json

ここでは東京リージョンを選択しています

接続確認
$ aws ec2 describe-instances

適当なコマンドを打ってエラーが出なければOKです

2. Serverlessをインストールしてプロジェクトを作成してデプロイ

serverlessのインストール
$ npm install serverless -g
$ sls -v
1.24.1

この時点でのバージョンは1.24.1

プロジェクトの作成
$ sls create -t aws-python3 -n my-misoca-serverless

my-misoca-serverlessという名前でプロジェクトを作成しました。
テンプレートとしてaws-python3を使っています。
コマンドを実行したディレクトリに serverless.yaml と handler.pyが作成されます。

serverless.yamlにデプロイ先のリージョンを指定します。
(以下のコードではテンプレートで作成されたものからコメントを削除しています)

serverless.yaml
service: my-misoca-serverless

provider:
  name: aws
  runtime: python3.6

functions:
  hello:
    handler: handler.hello

デプロイする前にローカルで関数の実行を確認します

ローカルでhello関数を実行
$ sls invoke local -f hello
{
    "statusCode": 200,
    "body": "{\"message\": \"Go Serverless v1.0! Your function executed successfully!\", \"input\": {}}"
}

デプロイします。

デプロイ
$ sls deploy -v
...
Service Information
service: my-misoca-serverless
stage: dev
region: ap-northeast-1
stack: my-misoca-serverless-dev
api keys:
  None
endpoints:
  None
functions:
  hello: my-misoca-serverless-dev-hello

Stack Outputs
HelloLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:535395497623:function:my-misoca-serverless-dev-hello:1
ServerlessDeploymentBucketName: my-misoca-serverless-dev-serverlessdeploymentbuck-uy0rkalebbwr

出力は省略していますが、Lambdaが作成されました。
作成されたLambdaをコンソールで確認

Cloud Formationも確認。
Cloud Formation

リモートの関数を呼び出して確認します。

リモートでhello関数を実行
$ sls invoke -f hello -l                                                                                               
{
    "statusCode": 200,
    "body": "{\"message\": \"Go Serverless v1.0! Your function executed successfully!\", \"input\": {}}"
}
--------------------------------------------------------------------
START RequestId: 6119c8f6-d1f2-11e7-b010-7b05d8617e25 Version: $LATEST
END RequestId: 6119c8f6-d1f2-11e7-b010-7b05d8617e25
REPORT RequestId: 6119c8f6-d1f2-11e7-b010-7b05d8617e25  Duration: 0.30 ms       Billed Duration: 100 ms         Memory Size: 1024 MB    Max Memory Used: 22 MB

handler.pyに定義されたhello関数が呼び出されています。
(以下のコードではテンプレートで作成されたものからコメントを削除しています)

handler.py
import json


def hello(event, context):
    body = {
        "message": "Go Serverless v1.0! Your function executed successfully!",
        "input": event
    }

    response = {
        "statusCode": 200,
        "body": json.dumps(body)
    }

    return response

念のためCloud Watchでログを確認します。
スクリーンショット 2017-11-26 0.10.58.png

デプロイするごとにログストリームが作成されます。
スクリーンショット 2017-11-26 0.12.06.png

スクリーンショット 2017-11-26 0.12.59.png

実行が確認できました。
ちなみに、pythonのprint関数などを使った出力はここに出力されますので、デバッグに重宝します。

3. Misoca APIを使う

Misoca : https://www.misoca.jp/

3.1. MisocaからAPIキーを取得

MisocaからOAuth2に使うAPIキーを取得するのですが、その前にOAuth2認証時に呼び出されるコールバック(内部は後で実装)をAPI GatewayとLambdaで作成します。

serverless.yaml内に記述を追加します。

serverless.yamlの一部
functions:

  misoca_callback:
    handler: handler.misoca_callback
    events:
      - http:
          path: misoca_callback
          method: get

handler.pyに関数を作成します

handler.pyの一部
def misoca_callback(event, context):
    print(event)
    
    response = {
        "statusCode": 200,
        "body": "misoca_callback"
    }

    return response

デプロイします。

デプロイ
$ sls deploy -v
...
Serverless: Stack update finished...
Service Information
service: my-misoca-serverless
stage: dev
region: ap-northeast-1
stack: my-misoca-serverless-dev
api keys:
  None
endpoints:
  GET - https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/misoca_callback
functions:
  hello: my-misoca-serverless-dev-hello
  misoca_callback: my-misoca-serverless-dev-misoca_callback

Stack Outputs
HelloLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:535395497623:function:my-misoca-serverless-dev-hello:2
MisocaUnderscorecallbackLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:535395497623:function:my-misoca-serverless-dev-misoca_callback:1
ServiceEndpoint: https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev
ServerlessDeploymentBucketName: my-misoca-serverless-dev-serverlessdeploymentbuck-uy0rkalebbwr

ここで、以下の部分のhttpsからがコールバック先のURLになるので記録しておきます。

エンドポイントのURL
endpoints:
  GET - https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/misoca_callback

Misocaにログインした状態で次のURL(新しいアプリケーションの登録)にアクセスします。
https://app.misoca.jp/oauth2/applications/new

新しいアプリケーションの登録
先程取得したURLとローカルテスト用のURIを Redirect URIに設定します。

スクリーンショット 2017-11-26 0.33.35.png

アプリケーションが作成されました。Application IDとSecretがAPIキーとして必要ですので記録しておきます。

3.2. serverless-python-requirements プラグインと必要な外部ライブラリのインストール

pythonでOAuth2クライアントを実装する(及びその他)ためにライブラリを使います。
Lambdaに用意されていないライブラリを使うためにserverless-python-requirementsというプラグインを利用します。

serverless-python-requirements:
https://github.com/UnitedIncome/serverless-python-requirements

次のコマンドでserverless-python-requirementsをインストールします。

serverless-python-requirementsのインストール
$ sls plugin install -n serverless-python-requirements

serverless.yamlに記述が追加され、package.jsonが作成されます。

serverless.yamlの一部
plugins:
  - serverless-python-requirements
package.json
{
  "name": "my-misoca-serverless",
  "description": "",
  "version": "0.1.0",
  "dependencies": {},
  "devDependencies": {
    "serverless-python-requirements": "^3.0.10"
  }
}

requirements.txtを作成してserverless-python-requirementsがインストールするライブラリを記述します。

requirements.txtの作成
$ touch requirements.txt
requirements.txt
requests
requests-oauthlib
boto3

同様のライブラリをCloud9上にもインストールしておきます。

pipでライブラリをインストール
$ sudo -H pip install requests requests-oauthlib boto3

3.3. OAuth2クライアントの実装(認証トークンの取得)

ライブラリ読み込みの宣言と定数の定義を先に記載しておきます。
後で説明しますが、OAuth2の認証情報はS3に保存しますのでバケットの名前を決めてます。

handler.pyの一部
import json

from datetime import datetime
import tempfile

import urllib.parse
import boto3
import botocore

from requests_oauthlib import OAuth2Session


MISOCA_APPLICATION_ID='<Your Misoca Application ID>'
MISOCA_APP_SECRET_KEY='<Your Misoca Application Secrent>'
MISOCA_END_POINT='https://app.misoca.jp/api'
MISOCA_AUTHORIZE_URI='https://app.misoca.jp/oauth2/authorize'
MISOCA_TOKEN_URI='https://app.misoca.jp/oauth2/token'
MISOCA_REDIRECT_URI = 'https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/misoca_callback'

BUCKET_NAME = 'my-misoca-service-bucket'
FILE_NAME = 'credential.json'

handler.pyに認証を行う関数を作成します。
ローカル実行時はテスト用のリダイレクトURIを使用するようにしています。
ここでは認証用のURLを取得し、そのURLにリダイレクトさせています。
responseはAPI Gatewayが受け取るフォーマットに合わせています。
今回はscopeにreadを指定していますが書き込みが必要なAPIを使う場合はwriteを指定すればいいと思います(未確認)

handler.pyの一部
def authorize(event, context):
    
    redirect_uri = MISOCA_REDIRECT_URI
    
    if type(context).__name__ == 'FakeLambdaContext':
        redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
    
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri=redirect_uri, scope='read')
    
    authorization_url, state = client.authorization_url(MISOCA_AUTHORIZE_URI)
    
    response = {
        'statusCode': 301,
        'headers': {
            "Location" : authorization_url
        },
        'body': None
    };
    
    return response

serverless.yamlにラムダ関数の記述を追加(後でAPI Gatewayのための記述を追加します)

serverless.yamlの一部
functions:
  authorize:
    handler: handler.authorize
ローカル実行して確認
$ sls invoke local -f authorize -l
{
    "statusCode": 301,
    "headers": {
        "Location": "https://app.misoca.jp/oauth2/authorize?response_type=code&client_id=<your-client-id-here>&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=read&state=FtfNpTOGSlPGopnDSHnhQbp1WtUAyE"
    },
    "body": null
}

Location部分のURLをブラウザで開くとMisocaでの認証が表示されます。
スクリーンショット 2017-11-26 1.31.07.png

認証すると認証コードが表示されます(このコードは一度しか使えません)
スクリーンショット 2017-11-26 1.31.34.png

認証コードから認証トークンの取得を行います。
ここで作成する関数はローカルテストでのみ使用します。

handler.pyの一部
def get_access_token(event, context):
    
    code = '28471cdfabce35d2cad93bdb8b26cdba40ac52b0803ddf35aed07984713ca591'
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri='urn:ietf:wg:oauth:2.0:oob', scope='read')
    dict = client.fetch_token(MISOCA_TOKEN_URI, code=code, client_secret=MISOCA_APP_SECRET_KEY)
    print(dict)
serverless.yamlの一部
functions:
  get_access_token:
    handler: handler.get_access_token
ローカル実行して確認
$ sls invoke local -f get_access_token -l
{'access_token': '<access token>', 'token_type': 'bearer', 'expires_in': 7776000, 'refresh_token': '<refresh token>', 'scope': ['read'], 'created_at': 1511628336, 'expires_at': 1519404310.5428352}

アクセストークンとリフレッシュトークンが取得できれば成功です。

トークンをあとで利用するためにS3への保存を実装します。
ついでにS3からの読み出しも実装します。

handler.pyの一部

def get_bucket(bucket):
    s3 = boto3.resource('s3') 
    b = s3.Bucket(bucket) 
    
    try:
        s3.meta.client.head_bucket(Bucket=bucket)
    except botocore.exceptions.ClientError as e:
        print ("The bucket doesn't exist. Creating...")
        b.create(CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-1' })
        
    return b
    
def save_token(dict):
    
    data = json.dumps(dict, sort_keys=True, indent=4)
    
    tmp = tempfile.NamedTemporaryFile()
    with open(tmp.name, 'w') as f:
        f.write(data)
        f.flush()
        bucket = get_bucket(BUCKET_NAME)
        res = bucket.upload_file(f.name, FILE_NAME)
        
def load_token():
    bucket = get_bucket(BUCKET_NAME)
    tmp_file = '/tmp/'+FILE_NAME
    try:
        bucket.download_file(FILE_NAME, tmp_file)
        data = json.load(open(tmp_file))
        return data
    except botocore.exceptions.ClientError as e:
        return None

LambdaからS3にバケットを作成したりファイルを読み書きするために IAM Role の許可を与えます。

serverless.yamlの一部
provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:CreateBucket"
        - "s3:ListBucket"
        - "s3:PutObject"
        - "s3:GetObject"
      Resource:
        - "arn:aws:s3:::my-misoca-serverless-bucket"
        - "arn:aws:s3:::my-misoca-serverless-bucket/*"

get_access_tokenを修正してバケットへのトークンの保存を確認します。
(コードは新しく取得する必要があります)

handler.pyの一部
def get_access_token(event, context):
    
    code = '28471cdfabce35d2cad93bdb8b26cdba40ac52b0803ddf35aed07984713ca591'
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri='urn:ietf:wg:oauth:2.0:oob', scope='read')
    dict = client.fetch_token(MISOCA_TOKEN_URI, code=code, client_secret=MISOCA_APP_SECRET_KEY)
    save(dict)

AWSのコンソールでS3にトークンが保存されていることを確認します。
(ダウンロードして中身を確認)

スクリーンショット 2017-11-26 2.14.12.png

3.4. OAuth2クライアントの実装(コールバックの実装)

misoca_callback関数を実装します。

handler.pyの一部
def misoca_callback(event, context):
    
    code = event['queryStringParameters']['code']
    
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri=MISOCA_REDIRECT_URI, scope='read')
    dict = client.fetch_token(MISOCA_TOKEN_URI, code=code, client_secret=MISOCA_APP_SECRET_KEY)
    
    save_token(dict)

    return {
      'statusCode': 200,
      'headers': {
      },
      'body': json.dumps(dict)
    };

authorizeのAPI Gateway記述を追加します。

serverless.yamlの一部
functions:
  authorize:
    handler: handler.authorize
    events:
      - http:
          path: authorize
          method: get
    

デプロイします。
(__pycache__ が存在するとデプロイが失敗する場合は __pycache__ を削除する)

デプロイ
$ sls deploy -v
...
endpoints:
  GET - https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/misoca_callback
  GET - https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/authorize
...

authorizeのURLをブラウザで開いて認証する。

https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/authorize

リダイレクトされて認証トークンが表示されれば成功

3.5. Misoca APIを呼び出す

/v1/invoices を呼び出してみます。

handler.pyの一部
def get_invoices(event, context):
    
    invoices = get('/v1/invoices')
    for invoice in invoices:
        print(invoice)
        
    return {
      'statusCode': 200,
      'headers': {
      },
      'body': json.dumps(invoices, sort_keys=True, indent=4)
    };
        
def get(function):
    
    client = get_client()
    if client != None:
        res = client.get(MISOCA_END_POINT+function)
        data = json.loads(res.content)
        return data
    else:
        return None

def get_client():
    extra = {
        'client_id': MISOCA_APPLICATION_ID,
        'client_secret': MISOCA_APP_SECRET_KEY,
    }

    def token_updater(token):
        save_token(token)

    token = load_token()
    if token != None:
        client = OAuth2Session(MISOCA_APPLICATION_ID,
                               token=token,
                               auto_refresh_kwargs=extra,
                               auto_refresh_url=MISOCA_TOKEN_URI,
                               token_updater=token_updater
                               )
        return client
    else:
        return None
serverless.yamlの一部
functions:
  get_invoices:
    handler: handler.get_invoices
    events:
      - http:
          path: get_invoices
          method: get

デプロイしてURLを開くと請求書の一覧が取得できます。

https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/get_invoices

まとめ

軽い気持ちで書き始めたらすっかり長くなってしまいましたが、せっかくなのでまとめてみました。

Serverlessは個人的に興味津々でどんどん使っていきたいのですが、Cloud Watchのログを見たり、ローカル実行したりを駆使することでデバッグしながら書いていくのですが、どこを見れば良いのかを知るまでが大変ですね。

一番ハマったのはLambdaでは成功するのにでAPI Gateway経由で実行すると"Internal server error"のみが表示されたときでした。これはreturnで返す連想配列がAPI Gatewayの形式に従ってなかったからなのですが、Cloud Watchのログではエラーメッセージが出ないため、見つけるのに苦労しました。最終的にはAPI Gatewayのテスト機能を使うことで解決しました。

最後に、serverless.yamlとhandler.pyの最終状態を記載しておきます。

serverless.yaml
service: my-misoca-serverless

provider:
  name: aws
  runtime: python3.6
  region: ap-northeast-1
  
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:CreateBucket"
        - "s3:ListBucket"
        - "s3:PutObject"
        - "s3:GetObject"
      Resource:
        - "arn:aws:s3:::my-misoca-serverless-bucket"
        - "arn:aws:s3:::my-misoca-serverless-bucket/*"

functions:
  hello:
    handler: handler.hello
    
  misoca_callback:
    handler: handler.misoca_callback
    events:
      - http:
          path: misoca_callback
          method: get
          
  authorize:
    handler: handler.authorize
    events:
      - http:
          path: authorize
          method: get
          
  get_invoices:
    handler: handler.get_invoices
    events:
      - http:
          path: get_invoices
          method: get
    
  get_access_token:
    handler: handler.get_access_token

plugins:
  - serverless-python-requirements

handler.py
import json

from datetime import datetime
import tempfile

import urllib.parse
import boto3
import botocore

from requests_oauthlib import OAuth2Session


MISOCA_APPLICATION_ID='<your misoca application id>'
MISOCA_APP_SECRET_KEY='<your misoca application secret>'
MISOCA_END_POINT='https://app.misoca.jp/api'
MISOCA_AUTHORIZE_URI='https://app.misoca.jp/oauth2/authorize'
MISOCA_TOKEN_URI='https://app.misoca.jp/oauth2/token'
MISOCA_REDIRECT_URI = 'https://bvu8tvkkva.execute-api.ap-northeast-1.amazonaws.com/dev/misoca_callback'

BUCKET_NAME = 'my-misoca-serverless-bucket'
FILE_NAME = 'credential.json'

def misoca_callback(event, context):
    
    code = event['queryStringParameters']['code']
    
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri=MISOCA_REDIRECT_URI, scope='read')
    dict = client.fetch_token(MISOCA_TOKEN_URI, code=code, client_secret=MISOCA_APP_SECRET_KEY)
    
    save_token(dict)

    return {
      'statusCode': 200,
      'headers': {
      },
      'body': json.dumps(dict)
    };


def hello(event, context):
    body = {
        "message": "Go Serverless v1.0! Your function executed successfully!",
        "input": event
    }

    response = {
        "statusCode": 200,
        "body": json.dumps(body)
    }

    return response
    
def get_access_token(event, context):
    
    code = '4eb83b39f75c9fbf5254e3a683e5472f6347a6c7e3dbbfa15b514a72376aee28'
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri='urn:ietf:wg:oauth:2.0:oob', scope='read')
    dict = client.fetch_token(MISOCA_TOKEN_URI, code=code, client_secret=MISOCA_APP_SECRET_KEY)
    save_token(dict)

def authorize(event, context):
    
    redirect_uri = MISOCA_REDIRECT_URI
    
    if type(context).__name__ == 'FakeLambdaContext':
        redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
        
    print(redirect_uri)
    
    client = OAuth2Session(MISOCA_APPLICATION_ID, redirect_uri=redirect_uri, scope='read')
    
    authorization_url, state = client.authorization_url(MISOCA_AUTHORIZE_URI)
    
    response = {
        'statusCode': 301,
        'headers': {
            "Location" : authorization_url
        },
        'body': None
    };
    
    return response

def get_invoices(event, context):
    
    invoices = get('/v1/invoices')
    for invoice in invoices:
        print(invoice)
        
    return {
      'statusCode': 200,
      'headers': {
      },
      'body': json.dumps(invoices, sort_keys=True, indent=4)
    };
        
def get(function):
    
    client = get_client()
    if client != None:
        res = client.get(MISOCA_END_POINT+function)
        data = json.loads(res.content)
        return data
    else:
        return None

def get_client():
    extra = {
        'client_id': MISOCA_APPLICATION_ID,
        'client_secret': MISOCA_APP_SECRET_KEY,
    }

    def token_updater(token):
        save_token(token)

    token = load_token()
    if token != None:
        client = OAuth2Session(MISOCA_APPLICATION_ID,
                               token=token,
                               auto_refresh_kwargs=extra,
                               auto_refresh_url=MISOCA_TOKEN_URI,
                               token_updater=token_updater
                               )
        return client
    else:
        return None

def get_bucket(bucket):
    s3 = boto3.resource('s3') 
    b = s3.Bucket(bucket) 
    
    try:
        s3.meta.client.head_bucket(Bucket=bucket)
    except botocore.exceptions.ClientError as e:
        print ("The bucket doesn't exist.")
        b.create(CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-1' })
        
    return b
    
def save_token(dict):
    
    data = json.dumps(dict, sort_keys=True, indent=4)
    
    tmp = tempfile.NamedTemporaryFile()
    with open(tmp.name, 'w') as f:
        f.write(data)
        f.flush()
        bucket = get_bucket(BUCKET_NAME)
        res = bucket.upload_file(f.name, FILE_NAME)
        
def load_token():
    bucket = get_bucket(BUCKET_NAME)
    tmp_file = '/tmp/'+FILE_NAME
    try:
        bucket.download_file(FILE_NAME, tmp_file)
        data = json.load(open(tmp_file))
        return data
    except botocore.exceptions.ClientError as e:
        return None
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?