Download as pdf or txt
Download as pdf or txt
You are on page 1of 18

An approach to NF Registration

MBK Vamsi (EE16B023) & Sachin S (EE16B037)


Dept. of Electrical Engineering

Indian Institute of Technology, Madras

27th June - 27th July

1
Contents:

1. Abstract

2. Introduction

3. Cellular Networks: Overview

4. 5G Core Network's Service Based Interface

5. Implementing a RESTful API

(a) What is RESTful API?

(b) Understanding server-client model: Apache.

(c) Building a server-client model: Python Flask and CURL.

(d) All-Python server-client model: Python Flask and Python Requests.

6. Open API

7. NF Registration

8. Conclusion

9. Future study

2
Abstract

NF Registration in the 5G core network using the Open API specication as mentioned by 3GPP.

Introduction

The below describes the service based architecture in 5G core network.It has many network functions
namely NRF, NEF, PCF etc.

Figure 1: Figure showing the NFs and how they are connected via SBI

We aim to understand the method of registering an NF into NRF in the above architecture and also
acheive the same.

Cellular Networks: Overview

Mobile network or Cellular network is a communication network in which the last link is wireless. Our
present communication devices like smartphones work on this concept. Thus the name 'cell' phone.
Characteristics of a cellular network:

• A large area gets divided into cells, each having its own transceiver (base transceiver stations).

• Low powered transmitters (~100W) are used so that cellphones don't use too much power and their
size is minimal.

• Several cells together help us cover wide geographical areas. The density of cells in a given area
largely depend on the population of the area, for example cities have more cells in a given area
while rural regions have lesser.

Topology of a typical cellular network:

3
From the above diagram we nd that each cell is regularly shaped, usually hexagonal. Each cell is
assigned a frequency and a single user can use a single frequency of the cell. Users connect to the nearest
cell available and use the corresponding frequency. If we have more users than the number of frequencies
available we use any of the following technologies to accomodate all the users.

1. TDMA (Time division multiple access):

2. FDMA (Frequency division multiple access):


Each user is allocated a dierent frequency in the band available.

3. CDMA (Code division multiple access):


Special coding system is implemented for transmitting and receiving in the same frequency.

There are other types of technologies like OFDMA, WCDMA etc which are used in later cellular network
implementations like LTE.

5G core network's Service Based Interface

As shown in the gure below the service based architecture is made up of many network functions each
of them are communicated using a denite set of rules. These set of rules is called as REST API or
RESTful API which will be disucussed later.
Instead of predened interfaces between elements (NFs), services model is used. Components query an
NF Repository Function (NRF) to discover and communicate with each other. This communication fol-
lows REST API.

4
API - Application programming interface:
Provides an interface between two systems. In our case, the two systems are NFs that interact program-
matically through the API. Developers use API calls behind the scenes to pull information into their
apps. For example in youtube app API pulls the video into our app from youtube.com server ars per
user request.

Implementing a REST API

REST: Representational state transfer.


REST is a web standards based architecture and uses HTTP Protocol for data communication. There
exists a set of 6 guiding principles.

1. Clientserver: Each system consists of a client and a server which are indeed a physical computer
and client can request server based on its requirements.

2. Stateless: None of the clients context is to be stored on the server side between the request. All of
the information necessary to service the request is contained in the URL, query parameters, body
or headers i.e. URL should be self sucient.

3. Cacheable: Clients can cache the responses i.e. frequently accessed resources can be stored locally
on the client side for faster access of resources.

4. Uniform interface: By applying the software engineering principle of generality to the component
interface, the overall system architecture is simplied and the visibility of interactions is improved.
In order to obtain a uniform interface, multiple architectural constraints are needed to guide the
behavior of components.

5. Layered system: The layered system style allows an architecture to be composed of hierarchical
layers by constraining component behavior such that each component cannot see beyond the
immediate layer with which they are interacting.

6. Code on demand: REST allows client functionality to be extended by downloading and executing
code in the form of applets or scripts. This simplies clients by reducing the number of features
required to be pre-implemented.

Understanding server-client model: Apache


Setting up an Apache server:

Run the following commands in the terminal to install apache2

sudo apt update


sudo apt install apache2

5
After letting the command run, all required packages are installed and we can test it out by typing in
our IP address for the web server.
Type the following to get your IP address.

ifconfig

We see the page above in our web browser upon typing our IP address, it means that Apache has
been successfully installed on our server. This is a default page for your server, now we create our own.
Creating a directory so that we can place all our les.

sudo mkdir -p / var / www / tapt / public

'tapt' and 'public' are our directories, its up to the user to name them.
We need to grant permissions to this directory.

sudo chown -R $USER : $USER / var / www / tapt / public


sudo chmod -R 775 / var / www

Go to the created directory 'public' so that we can add les to retrieve from the client side. Create a le
named 'index.html' and write your own html code. We wrote the following in our 'index.html':

< html >


< head >
< title > Ubuntu rocks ! </ title >
</ head >
< body >
<p > I ' m running this website on an Ubuntu server !
</p >
</ body >
</ html >

6
Now we create a virtual host le. Apache already has a default virtual host le named '000-default.conf '
which can be accessed at '/etc/apache2/sites-available'. Add the below lines to a newly created le
'tapt.conf ' in the same directory.

< VirtualHost *:80 >

ServerAdmin johndoe@tapt . com


ServerName tapt . pp . com
ServerAlias tapt . pp . com
DocumentRoot / var / www / tapt / public
ErrorLog $ { APACHE_LOG_DIR }/ error . log
CustomLog $ { APACHE_LOG_DIR }/ access . log combined

</ VirtualHost >

# vim : syntax = apache ts =4 sw =4 sts =4 sr noet

This enables us to link our IP address to a server name which is 'tapt.pp.com'. We can type this instead
of our IP address in the browser. Within the directory type the following to enable our .conf le:

sudo a2ensite tapt . conf

Let us disable the default apache .conf le:

sudo a2dissite 000 - default . conf

Reload apache2 server:

sudo service apache2 restart

Next we get to the hosts le at '/etc/hosts' so that we can say to our computer that IP is linked with
the domain name 'tapt.pp.comm'. If we try to access the page from a dierent computer we need to do
this in that computer too. Add the following line in the 'hosts' le.

192.168.43.70 tapt . pp . com

Here, '192.168.43.70' should be replaced by the IP shown by the 'ifcong' command on the terminal.
After this upon typing tapt.pp.com on the web browser (client) on any computer connected to the same
network, we get the following:

One has to note that here we are merely obtaining the resource from the server at the url 'tapt.pp.com'
which is in HTML format, the client here is our web browser. But a REST API is something more than
that, we should be able to create resources and modify them too, using HTTP methods (GET, PUT,
POST, DELETE). So in the next subsection we will see how to build a REST API server in python which
lets us do all these.

7
Building a server-client model: Python Flask and CURL
We will be building this code in a virtual environment, so that there is no machine incompatibility issues.
Setting up virtual environment after creating a directory for the server:

mkdir todo - api


cd todo - api
virtualenv flask

Here again 'todo-api' and 'ask' are not keywords, upto the user to choose.
We will be using a web framework module for python which is called 'Flask' which enables us to build an
API without the need to bother about complicated protocols underlying. Installing ask in the virtual
environment:

f l a s k / bin / pip install flask

Now that we have Flask installed let's create a simple web application, which we will put in a le called
app.py.

#! flask / bin / python


from flask import Flask

app = Flask ( __name__ )

@app . route ( '/ ')


def index ():
return " Hello , World !"

if __name__ == ' __main__ ':


app . run ( host = 0.0.0.0 , port =5000 , debug = True )

Now we have to run this application. Follow the below commands in terminal to execute app.py:

$ chmod a + x app . py
$ ./ app . py -- host = 0.0.0.0
* Running on http ://127.0.0.1:5000/
* Restarting with reloader

Now you can launch your web browser and see the application in action.
Now let us go a little deeper and make some improvements in our application.

#! flask / bin / python


from flask import Flask , jsonify

app = Flask ( __name__ )

tasks = [
{
'id ': 1 ,
' title ': u ' Buy groceries ' ,
' description ': u ' Milk , Cheese , Pizza , Fruit , Tylenol ' ,
' done ': False
},
{
'id ': 2 ,
' title ': u ' Learn Python ' ,
' description ': u ' Need to find a good Python tutorial on the web ' ,

8
' done ': False
}
]

@app . route ( '/ todo / api / v1 .0/ tasks ' , methods =[ ' GET '])
def get_tasks ():
return jsonify ({ ' tasks ': tasks })

if __name__ == ' __main__ ':


app . run ( host = 0.0.0.0 , port =5000 , debug = True )

The above one is the example code for a todo-tasks application which has some information like title,
description,task_ID and a boolean variable which says whether the task is nished or not. The above is
the code for GET function which lists out all the tasks in the application.As you can see not much has
changed. Instead of the index entry point we now have a get_tasks function that is associated with the
'/todo/api/v1.0/tasks' URI, and only for the 'GET HTTP' method. Instead of web browser we can also
use 'curl' commands in terminal so that we can generate any type of HTTP requests.
The following is what we get when we run the given curl command:

$ curl -i http :// localhost :5000/ todo / api / v1 .0/ tasks


HTTP /1.0 200 OK
Content - Type : application / json
Content - Length : 294
Server : Werkzeug /0.8.3 Python /2.7.3
Date : Mon , 20 May 2013 04:53:53 GMT

{
" tasks ": [
{
" description ": " Milk , Cheese , Pizza , Fruit , Tylenol " ,
" done ": false ,
" id ": 1 ,
" title ": " Buy groceries "
},
{
" description ": " Need to find a good Python tutorial on the
web " ,
" done ": false ,
" id ": 2 ,
" title ": " Learn Python "
}
]
}

Now we shall go a bit deeper and implement second type of get method to get the details of a single
resource.

from flask import abort

@app . route ( '/ todo / api / v1 .0/ tasks / < int : task_id > ' , methods =[ ' GET '])
def get_task ( task_id ):
task = [ task for task in tasks if task [ ' id '] == task_id ]
if len ( task ) == 0:
abort (404)
return jsonify ({ ' task ': task [0]})

And as per this our curl request also changes its form only the URI part is changed.
Next in our list is the POST method, which we will use to insert a new item in our task database:

9
from flask import request

@app . route ( '/ todo / api / v1 .0/ tasks ' , methods =[ ' POST '])
def create_task ():
if not request . json or not ' title ' in request . json :
abort (400)
task = {
'id ': tasks [ -1][ ' id '] + 1 ,
' title ': request . json [ ' title '] ,
' description ': request . json . get ( ' description ' , "") ,
' done ': False
}
tasks . append ( task )
return jsonify ({ ' task ': task }) , 201

Adding a new task is also pretty easy. The request.json will have the request data, but only if it came
marked as JSON. We then create a new task dictionary, using the id of the last task plus one (a cheap
way to guarantee unique ids in our simple database). We tolerate a missing description eld, and we
assume the done eld will always start set to False.
Now enter this curl command to execute the POST method in the application:

$curl -i -H " Content - Type : application / json " -X POST -d '{" title
":" Read a book "} ' http :// localhost :5000/ todo / api / v1 .0/ tasks

This is what we get when we run that command

HTTP /1.0 201 Created


Content - Type : application / json
Content - Length : 104
Server : Werkzeug /0.8.3 Python /2.7.3
Date : Mon , 20 May 2013 05:56:21 GMT

{
" task ": {
" description ": "" ,
" done ": false ,
" id ": 3 ,
" title ": " Read a book "
}
}

201 Created is the status code which says that the tasks dictionary has been appended. So as of now we
completed two types of GET method and also the POST method. PUT and DELETE methods follow
similar code and can be done easily. The remaining two functions of the web server are as given below:

@app . route ( '/ todo / api / v1 .0/ tasks / < int : task_id > ' , methods =[ ' PUT
'])
def update_task ( task_id ) :
task = [ task for task in tasks if task [ ' id '] == task_id ]
if len ( task ) == 0:
abort (404)
if not request . json :
abort (400)
if ' title ' in request . json and type ( request . json [ ' title '])
!= unicode :
abort (400)

10
if ' description ' in request . json and type ( request . json [ '
description ']) is not unicode :
abort (400)
if ' done ' in request . json and type ( request . json [ ' done ']) is
not bool :
abort (400)
task [0][ ' title '] = request . json . get ( ' title ' , task [0][ ' title
'])
task [0][ ' description '] = request . json . get ( ' description ' ,
task [0][ ' description '])
task [0][ ' done '] = request . json . get ( ' done ' , task [0][ ' done '])
return jsonify ({ ' task ': task [0]})

@app . route ( '/ todo / api / v1 .0/ tasks / < int : task_id > ' , methods =[ '
DELETE '])
def delete_task ( task_id ) :
task = [ task for task in tasks if task [ ' id '] == task_id ]
if len ( task ) == 0:
abort (404)
tasks . remove ( task [0])
return jsonify ({ ' result ': True })

In the update_task as u can see it has exhaustive of checking the input request just to make sure that
anything that the client provided us is in the expected format before we incorporate it into our database.
And the delete_task is a no surprise.
As of now we are only using curl to send requests to the server, now we shall and make
our own python client with the help of the 'requests' module in python.

All-Python server-client model: Python Flask and Python Requests


The following is the code for client python le.

#! flask / bin / python


import requests
import json

# for method POST :


response = requests . post (" http ://192.168.43.70:5000/ todo / api / v1
.0/ tasks " , json ={" Title ":" Attend interview "})

# for method PUT :


response = requests . put (" http ://192.168.43.70:5000/ todo / api / v1
.0/ tasks /3" , json ={" Title ":" Complete intern documentation "})

# for method GET :


response = requests . get (" http ://192.168.43.70:5000/ todo / api / v1
.0/ tasks ")

# for method DELETE :


response = requests . delete (" http ://192.168.43.70:5000/ todo / api /
v1 .0/ tasks /4")

print response . json ()

The above code does all the REST methods, we can keep the lines corresponding to only required methods
and remove the rest. The output looks similar to that of CURL, once the code is executed in terminal.

11
Open API

The OpenAPI Specication is a community-driven open specication within the OpenAPI Initiative, a
Linux Foundation Collaborative Project.
The OpenAPI Specication (OAS) denes a standard, programming language-agnostic interface de-
scription for REST APIs, which allows both humans and computers to discover and understand the
capabilities of a service without requiring access to source code, additional documentation, or inspection
of network trac.
In simple words, a given REST API will have a document whose contents(usually in YAML for-
mat) follow the rules laid down by Open API. This document completely 'describes' the API without
the actual implemenation shown. Users can use this document to develop code for their API in any
language/platform they wish.
The set of rules to be followed while documenting an API or to understand an already documented
API, visit the following link:
https://github.com/OAI/OpenAPI-Specication/blob/master/versions/3.0.1.md

Example for an Open API documentation:


Here's a simple pet-store API for which we have an Open API documentation.

Here, the rst line has the Open API version in which this le is written in. The 'info' object has
general characteristics about the API like the version of API, title and license. The 'paths' object has all
the URLs corresponding to the API and this object has details about all the methods(GET, PUT, POST
& DELETE), whichever is applicable for that URL. We can use this set of data from the documentation
to code our own API.
Let's see in the next section as to how we can use the Open API documentation of the NRF in 5G
core for NF Registration.

NF Registration

In NF Registration, we basically register any of NF into the NRF by providing its own description to the
NRF. The following diagram illustrates the NF Registration function:

12
We invoke the PUT method of the API, the body of this PUT request will contain the NFProle
which is stored in the NRF. Then the NRF returns a response with code 201 indicating a new resource
being created, the response body contains the NFProle created. All this information is also availabe in
the Open API documentation of the NRF.

Open API documentation of NRF:


Use the following keywords to nd the Open API documentation of the NRF online, in YAML format.

29510 NF Management yaml

This code contains a lot of functions apart from NFRegistration such as NFHeartbeat etc, in this report
we cover on the execution of NRF till NFRegistration.
The following are the URLs executed by us and their corresponding methods as specied by the above
documention:

Task Path Method Description


1 /nf-instances get Retrieves all NF
Instances
2 /nf-instances?nf-type={nf-type} get Retrieves NF instance
pointed by the nf-type
3 /nf-instances?limit={limit} get Retrieves all the NF
instances sequentially
as specied by the limit
4 /nf-instances/{nfInstanceID} get Read the prole of a
given NF Instance
specied by
nfInstanceID
5 /nf-instances/{nfInstanceID} put Register a new NF
Instance

The following code describes the server, the comments in the code fully de-
scribe the functionalities of each block of code
#! flask / bin / python
#!/ usr / bin / env python

'''
* This is an NRF model server which registers a client ( NF ) into
its database using 'GET ' method

13
* Also supports 'GET ' functions according to the 25910
_NRF_Management . yaml spec
* The whole of 25910 _NRF_Management . yaml spec is not implemented .
Only NF Registration part is complete .
* Last edited : 27 th July 2018
'''

# Importing flask modules


from flask import Flask , jsonify
from flask import abort
from flask import make_response
from flask import request
import sys

# Importing yaml parsing module


from ruamel . yaml import YAML
app = Flask ( __name__ )

# Function to load the . yaml file


def load_bgp_database ( filename ) :
''' load bgp database
we load our existing bgp information from the yaml file
generated from
our first script here in order to graph that information '''

# initialize our bgp db


bgp_db = {}

# load our existing bgp databses


with open ( filename , 'r ') as fn :
yaml = YAML ()
bgp_db = yaml . load ( fn )

return bgp_db

# loading the YAML file into a local structure


spec = load_bgp_database ( ' nrf_spec . yaml ') # takes the argument -
yaml file path

# array to store the NF Registration Data ( refer YAML file for


format )
NFRegistrationData = [
{
' heartBeatTimer ': None , # Heartbeat not implemented , so
None
' nfProfile ': { ' nfInstanceId ': u '4947 a69a - f61b -4 bc1 - b9da
-47 c9c5d14b64 ' , ' nfType ': u ' UDM ' , ' nfStatus ': u '
REGISTERED '}
},
{
' heartBeatTimer ': None ,
' nfProfile ': { ' nfInstanceId ': u ' ff746cf5 - c30b -4 d60 - ab80 -5
ea753063a41 ' , ' nfType ': u ' AMF ' , ' nfStatus ': u ' SUSPENDED '}
}
]

# Retrieves a collection of NF Instances


@app . route ( spec [ ' paths ']. keys () [0] , methods =[ ' GET '])

14
def get_NFs () :
lim = request . args . get ( ' limit ')
type = request . args . get ( ' nf - type ')
# returning limited set of NF instances
if lim != None and type == None :
nf_data =[]
for i in range ( int ( lim ) ) :
nf_data . append ( NFRegistrationData [ i ])
if len ( nf_data ) == 0:
abort (404)
return jsonify ({ ' NF Instances ( Store ) ': nf_data }) , 200
# returning a specific NF Instance corresponding to the
NFType specified in the URL
elif lim ==0 and type != None :
nf_data =[]
for data in NFRegistrationData :
if data [ ' nfProfile '][ ' nfType '] == type :
nf_data . append ( data )
if len ( nf_data ) == 0:
abort (404)
return jsonify ({ ' NF Instances ': nf_data }) , 200
return jsonify ({ ' NF Instances ': NFRegistrationData }) , 200

# Read the profile of a given NF Instance


@app . route ( spec [ ' paths ']. keys () [1]. replace ( '{ ' , ' < ') . replace
( '} ' , ' > ') , methods =[ ' GET '])
def get_NF ( nfInstanceID ) :
data = [ data for data in NFRegistrationData if data [ '
nfProfile '][ ' nfInstanceId '] == nfInstanceID ]
if len ( data ) == 0:
abort (403)
return jsonify ({ ' NF Instance ': data }) , 200 # care

# Register a new NF Instance


# This block assumes there could be multiple NFs with same NFType
but NFId should be unique
@app . route ( spec [ ' paths ']. keys () [1]. replace ( '{ ' , ' < ') . replace
( '} ' , ' > ') , methods =[ ' PUT '])
def register_NF ( nfInstanceID ) :
# checking for valid nfType in request body
if not request . json [ ' nfType '] in spec [ ' components '][ ' schemas
'][ ' NFType '][ ' anyOf '][0][ ' enum ']:
abort (400)
# checking for non empty request body
if not request . json :
abort (500)
k = 0
# NF update
for data in NFRegistrationData :
if data [ ' nfProfile '][ ' nfInstanceId '] == nfInstanceID :
NFRegistrationData [ k ][ ' nfProfile '][ ' nfInstanceId ']=
request . json [ ' nfInstanceId ']
NFRegistrationData [ k ][ ' nfProfile '][ ' nfType ']= request
. json [ ' nfType ']
NFRegistrationData [ k ][ ' nfProfile '][ ' nfStatus ']= u '
REGISTERED '
return jsonify ({ ' nfProfile ': data }) , 200
else :

15
k +=1
# NF Registration
if k == len ( NFRegistrationData ) :
data = {
' heartBeatTimer ': None ,
' nfProfile ': { ' nfInstanceId ': request . json [ ' nfInstanceId
'] , ' nfType ': request . json [ ' nfType '] , ' nfStatus ': u '
REGISTERED '}
}
NFRegistrationData . append ( data )
return jsonify ({ ' NFRegistrationData ': NFRegistrationData
}) , 201

# custom error handlers


@app . errorhandler (404)
def not_found ( error ) :
return make_response ( jsonify ({ ' error ': ' Not found '}) , 404)
@app . errorhandler (403)
def not_found ( error ) :
return make_response ( jsonify ({ ' error ': ' Forbidden '}) , 403)
@app . errorhandler (400)
def not_found ( error ) :
return make_response ( jsonify ({ ' error ': ' Bad Request '}) , 400)
@app . errorhandler (500)
def not_found ( error ) :
return make_response ( jsonify ({ ' error ': ' Internal Server
Error '}) , 500)
# server running at http :// localhost :5000
if __name__ == ' __main__ ':
app . run ( host = '0.0.0.0 ' , port =5000 , debug = True )

The following is the client code for all the tasks:


#! flask / bin / python
import requests
import json

# Task 1
response = requests . get (" http ://192.168.43.70:5000/ nf - instances
")

# To get some specified NFs as per query say limit or NFtype .

# Task 2
response = requests . get (" http ://192.168.43.70:5000/ nf - instances
? limit =1")

# Task 3
response = requests . get (" http ://192.168.43.70:5000/ nf - instances
? nf - type = NEF ")
# dont forget to replace the above IP with your own server IP

# Task 4
response = requests . get (" http ://192.168.43.70:5000/ nf - instances /
b665e57c -20 eb -4958 - bf65 -53 aebd915258 ")
# Instance ID in UUID format

16
# Task 5 = requests . put (" http ://192.168.43.70:5000/ nf - instances /
b665e57c -20 eb -4958 - bf65 -53 aebd915258 " , json ={" nfInstanceId ":"
b665e57c -20 eb -4958 - bf65 -53 aebd915258 " ," nfType ":" NEF " ,"
nfStatus ":" REGISTERED "})

print response . json ()

This client performs all the tasks, the user can select a specic task, comment the rest and run the code.

NF Registration output:
As mentioned earlier, the registration consists of a PUT request, whose body contains the NFProle; the
format in which NFProle has to be sent according to the documentation is:

NFProfile :{
" nfInstanceId ":{ Instance ID in UUID format } ,
" nfType ":{ NF Type }
" nfStatus ":" REGISTERED "
}

So, for registering an NF of nfInstanceId = b665e57c-20eb-4958-bf65-53aebd915258, and nfType = NEF;


the client code has the following line:

requests . put (" http ://192.168.43.70:5000/ nf - instances / b665e57c -20


eb -4958 - bf65 -53 aebd915258 " , json ={" nfInstanceId ":" b665e57c -20
eb -4958 - bf65 -53 aebd915258 " ," nfType ":" NEF " ," nfStatus ":"
REGISTERED "})

The corresponding output on the server:

It returns a '201' message, indicating the resource was created.


Output on the client side:

It returns the resource which was created from the server, indicating the NFRegistration was
successful.

Conclusion:

An overview of cellular networks was required to understand as a foreword to 5G network. The Service
Based Interface in 5G core network follows REST API, so in trying to understand the REST API client-
server methods and 'http' formats, several implementations of REST API was done (Apache, python ask,
curl, python requests). Python ask server with a python requests client was nalized to be the nal
modules for implementing the REST API. A simple pet-store REST API was developed. Studied about
OPEN API for NF Registration. Implemented NF Registration as per its OPEN API documentation.

17
Future work:

• The 'http' methods currently use the HTTP/1.1 version. The client and server has to be modied
to support HTTP/2 functionalities.

• The client is more like a simple terminal command. It has to be coded similar to the server(with
ask) and make it run as a server itself. So that both server and client can interchange roles when
the need arises.

• The whole of '29510 NF Management Services' documentation was not implemented, there are more
tasks like NF PATCH, NF heartbeat etc which can be implemented.

18

You might also like