Sunday, May 29, 2016

Introduce Python Classmethod and Staticmethod

classstatic

Normal Method

An simple way to use class in file cst_tuto.py

class tt(object):

    def __init__(self):
        print 'into init'
        self.a = 10

    def normethod(self):
        print 'in normal method'
        return 'L in normethod'

Adding the following lines at the end of the file, and execute it. ttt = tt() print ttt.normethod() You will get this result

root@ubuntu:# python cst_tuto.py
into init
in normal method
L in normethod

So simple, you have to declaim the class as a instance, ttt = tt().

But, is There a simple way to call normethod just like a class ? just like

tt.normethod()

The result is

root@ubuntu:# python cst_tuto.py
Traceback (most recent call last):
  File "cst_tuto.py", line 12, in <module>
    print tt.normethod()
TypeError: unbound method normethod() must be called with tt instance as first argument (got nothing instead)

It's not working.
Same question, we now ask again is there a simpole way to call normethod just like a class ? Yes, we can use staticmethod or classmethod. And I will show you what is the difference between them.

Staticmethod

Now we can modify cst_tuto.py as the followed.

class tt(object):

    def __init__(self):
        print 'into init'
        self.a = 10

    @staticmethod
    def stamethod():
        print 'in static'
        return 'L in static'

    def normethod(self):
        print 'in normal method'
        return 'L in normethod'

We now execute it

ttt = tt()
## it will access __init__
print ttt.normethod()
print '-------------------'
print tt.stamethod()

The result is

root@ubuntu:# python cst_tuto.py
into init
in normal method
L in normethod
-------------------
in static
L in static

Yes, the result is satisfied our expectation that staticmethod can directly access the class, and no instance needed anymore.

ClassMethod

Based on the previous used file cst_tuto.py, we add classmethod in it.

class tt(object):

    def __init__(self):
        print 'into init'
        self.a = 10

    @staticmethod
    def stamethod():
        print 'in static'
        return 'L in static'

    @classmethod
    def klamethod(cls):
        print 'in klassmethod'
        print cls
        return 'L in klamethod'

    def testmethod(self):
        print 'in the test method'
        return 'L in testmethod'

    def normethod(self):
        print 'in normal method'
        return 'L in normethod'

In the end of this file, and execute this python file.

print tt.klamethod()

The result is

root@ubuntu:# python cst_tuto.py
in klassmethod
<class '__main__.tt'>
L in klamethod

Of course it is satisfied our expectation again.
But what is cls. This is the only different between staticmethod and classmethod.

difference between staticmethod and classmethod

The staticmethod is a class that one can access it as a class not a instance. The classmethod is the same with staticmethod on this point, but classmethod can access the whole class tt by using cls, that staticmethod cannot do it. Staticmethod will be isolated and cannot contact to the other class.

The is an example that how the classmethod access the tt class. The hint is cls contains cls.__name__=tt that will help us to declaim the tt class.

class tt(object):

    def __init__(self):
        print 'into init'
        self.a = 10

    @staticmethod
    def stamethod():
        print 'in static'
        return 'L in static'

    @classmethod
    def klamethod(cls):
        print 'in klassmethod'
        ttt = cls()
        print ttt.testmethod()
        return 'L in klamethod'

    def testmethod(self):
        print 'in the test method'
        return 'L in testmethod'

    def normethod(self):
        print 'in normal method'
        return 'L in normethod'

print tt.klamethod()

Adding ttt=cls() that is equivlent to ttt=tt() but don't forget classmethod only have one way to connect to outside world that is cls.

The result is

root@ubuntu:~# python cst_tuto.py
in klassmethod
into init
in the test method
L in testmethod
L in klamethod

Yes, we can access testmethod in the tt class. It's perfect.

The other question is

how to access classmethod from a normal method

def normethod(self):
    # how to access klamethod
    tt.klamethod()
    print 'in normal method'
    return 'L in normethod'

You can access it just like the way to access classmethod.

how to access staticmethod from a classmethod

@classmethod
def klamethod(cls):
    print 'in klassmethod'
    print cls
    print 'use stamethod in klamethod'
    print cls.stamethod()

In class method you can access staitcmethod without declaim ttt=cls(), but using cls.stamethod(). Just like to access a staticmethod. But you cannot get init attibute, if you don't decliam it as a instance.

How to import them and the behavior of the staticmehtod and classmethod

Adding the file ii_tuto.py, but don't forget to create the file init.py.

from cst import tt as yy
yy.stamethod()
yy.klamethod()
s = yy()
s.testmethod()

yy.normethod()

We don't need to explain it too much, since it's similar to our explanation before.

More About Classmethod

As mentioned before, we emphisis the tt.__name__ is quite important concept using classmethod.

class Hero:

  @staticmethod
  def say_hello():
     print("Helllo...")

  @classmethod
  def say_class_hello(cls):
     if(cls.__name__=="HeroSon"):
        print("Hi Kido")
     elif(cls.__name__=="HeroDaughter"):
        cls.class1 = HeroDaughter()
        cls.class1.say_daughter_hello()
        print("Hi Princess")

class HeroSon(Hero):
  def say_son_hello(self):
     print("test  hello")

class HeroDaughter(Hero):
  def say_daughter_hello(self):
     print("test  hello daughter")

testson = HeroSon()
testson.say_class_hello()
testson.say_hello()
testdaughter = HeroDaughter()
testdaughter.say_class_hello()
testdaughter.say_hello()

The result is

"""
Hi Kido
Helllo...
Hi Princess
Helllo...
"""

We inherent Class Hero and as a base class, and we can implement method depends on the input type in base class.
In the method of say_class_hello can identify which subclass calling and execute different function programing by ourself.

More About This Example

Furthermore Adding the following line to the beginning of the cst_tuto.py.

print 'haha'
class tt(object):

    def __init__(self):
        print 'into init'
        self.a = 10

    @staticmethod
    def stamethod():
    .
    .
    .

Execute ii_tuto.py again

root@ubuntu:~/lab/python/classmethod# python ii_tuto.py
haha
in static
in klassmethod

You will see the result, when import cst the claim outside the class will be execute. Be careful about that, it will affect the performace alot, so clean it.

Thursday, May 26, 2016

OpenStack Keystone Fernet Key in Multiple Regions

keystonefernet_intro2

Introduce Keystone Fernet Token (2)

Based on my previous blog:

http://gogosatellite.blogspot.tw/2016/05/keystone-setup-fernet-token-and.html

One can using key-rotate tool: keystone-manage fernet_rotate

root@controller:~# keystone-manage fernet_rotate --keystone-user keystone --keystone-group keystone
2016-05-27 08:29:01.388 2400 WARNING keystone.token.providers.fernet.utils [-] [fernet_tokens] key_repository is world readable: /etc/keystone/fernet-keys/
2016-05-27 08:29:01.404 2400 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 2 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/1']
2016-05-27 08:29:01.404 2400 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 1
2016-05-27 08:29:01.405 2400 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 2
2016-05-27 08:29:01.406 2400 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 2
2016-05-27 08:29:01.408 2400 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0

We will explane that how is the key rotation later

root@controller:~# ls /etc/keystone/fernet-keys/
0  1  2
root@controller:~# cat /etc/keystone/fernet-keys/2
BIoWWHPzDcoNRhFsg3TFzrRHoYVlL1MECDreHBREqJo=
root@controller:~# cat /etc/keystone/fernet-keys/0
RuiOVQGerT_owvluKjEaN9mOzy52vTJLCujAyfYkCrQ=

Even using the same key, the token is still different, since the change is due to expired time that is related to created time. So the token is always different.

The following shows the new token after rotating the fernet keys.

junmeindeMacBook-Pro:openstack_api junmein$ bash getToken.sh
HTTP/1.1 201 Created
Date: Fri, 27 May 2016 01:02:20 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Subject-Token: gAAAAABXR5yfF-PkUJPVRFA2gCDsZS6YT5y9Ud4NeTd7RLRaxFq8dp_om3jdCdJI2-ncQ5CFDHqZDRhAaeIS3BlfXoqkE9CUBX4jdUf4k6WwwCm4nkH52kpAEAkJuXntfs9qxweWPESaGJ-_bFJGWsuk9MniJDMCWXulml4MLcNG8zsHc98cVsg%3D
Vary: X-Auth-Token
X-Distribution: Ubuntu
x-openstack-request-id: req-b942080e-b242-4199-bf79-31d8d3111900
Content-Length: 2065
Content-Type: application/json

{"token": {"methods": ["password"], "roles": [{"id": "9fe2ff9ee4384b1894a90878d3e92bab", "name": "_member_"}], "expires_at": "2016-05-27T02:02:22.229081Z", "project": {"domain": {"id": "default", "name": "Default"}, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}, "catalog": [{"endpoints": [{"region_id": "RegionOne", "url": "http://controller:35357/v2.0", "region": "RegionOne", "interface": "admin", "id": "02f744638b2f44edb28bd64d9894c34a"}, {"region_id": "RegionOne", "url": "http://controller:5000/v2.0", "region": "RegionOne", "interface": "public", "id": "34171612079f4b028fdb01e38ec04b8c"}, {"region_id": "RegionTwo", "url": "http://controller1:5000/v2.0", "region": "RegionTwo", "interface": "internal", "id": "5222cc1e398043ffae73f74caa66a451"}, {"region_id": "RegionTwo", "url": "http://controller1:5000/v2.0", "region": "RegionTwo", "interface": "public", "id": "55b13c6057c2401fbff156b1b3d81db1"}, {"region_id": "RegionTwo", "url": "http://controller1:35357/v2.0", "region": "RegionTwo", "interface": "admin", "id": "d68629a1967b4c81aa3717b0d4931458"}, {"region_id": "RegionOne", "url": "http://controller:5000/v2.0", "region": "RegionOne", "interface": "internal", "id": "dacd582311a641b9ade00421ab19e8a2"}], "type": "identity", "id": "97fb94ea818d452894d33b900781b98e", "name": "keystone"}, {"endpoints": [{"region_id": "RegionTwo", "url": "http://lala/v2", "region": "RegionTwo", "interface": "admin", "id": "34188152080646b99f974ed7b05b6a58"}, {"region_id": "RegionTwo", "url": "http://sasa/v2", "region": "RegionTwo", "interface": "internal", "id": "f02b47a39d7449c693982b837f739537"}, {"region_id": "RegionTwo", "url": "http://haha/v2", "region": "RegionTwo", "interface": "public", "id": "f3e80838241541138e6bc31b6528a7d2"}], "type": "service1", "id": "8f1ce2e503ba4fbcb095e8469200b8e4", "name": "service1"}], "extras": {}, "user": {"domain": {"id": "default", "name": "Default"}, "id": "d8b0e3b8824143b0b47b8155747fa560", "name": "newuser"}, "audit_ids": ["Bbkxxh9PTGqQzCGiOsf4iA"], "issued_at": "2016-05-27T01:02:23.000000Z"}}

export token and get tenant

junmein$ export TOKEN=gAAAAABXR5yfF-PkUJPVRFA2gCDsZS6YT5y9Ud4NeTd7RLRaxFq8dp_om3jdCdJI2-ncQ5CFDHqZDRhAaeIS3BlfXoqkE9CUBX4jdUf4k6WwwCm4nkH52kpAEAkJuXntfs9qxweWPESaGJ-_bFJGWsuk9MniJDMCWXulml4MLcNG8zsHc98cVsg%3D
junmein$ cat getTenant.sh
curl -H "X-Auth-Token: $TOKEN" http://172.16.235.128:5000/v2.0/tenants

To check Token validate or not by geting tenant information.

junmein$ bash getTenant.sh
{"tenants_links": [], "tenants": [{"description": "atesttenant", "enabled": true, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}]}

Key Rotation Method

Thanks for this blog, I just spend 5 mins I then can understand the key rotation method.

http://lbragstad.com/fernet-tokens-and-key-distribution/

We explan how the Key repository evolved from initial to roration.

We have 3 states

  • The Highest index (>=1) is primary (ready to become secondary, used for encryption and decryption)
  • The lower index (>0) is secondary (ready to purged, decryption only)
  • The lowest index(=0) is stage (ready to become primary, decryption only)

The evolution shown in following table.

Action Key State
initial 0 1
rotate 0 (new) 1 (old) 2 (from 0)
rotate1 0(new) 2(old) 3 (from 0) ;1(purged)
rotate2 0(new) 3(old) 4(from 0); 2(purged)
  • As we know stage 0 will become primary after rotate, so we note the primary with (from 0) to identify the highest index is from stage 0.
  • The number will be increased.
  • After becoming primary, the stage will be generated a new index 0 and with a new key.
  • If setting max key=3, after several rotation the lower index will be purged during the evolution.

Multiple Region testing using Fernet Token

In Region1: 172.16.235.128
In Region2: 172.16.235.159

Exp. 1:

Assume: We set the directory /etc/keystone/fernet-keys the same in both of region1 and region2. Assume: Keystone DB are the same for all the user information.

junmein$ cat getTenant2.sh
curl -H "X-Auth-Token: $TOKEN" http://172.16.235.159:5000/v2.0/tenants
junmein$ bash getTenant2.sh
{"tenants_links": [], "tenants": [{"description": "atesttenant", "enabled": true, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}]}junmeindeMacBook-Pro:openstack_api
junmein$ bash getTenant.sh
{"tenants_links": [], "tenants": [{"description": "atesttenant", "enabled": true, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}]}junmeindeMacBook-Pro:openstack_api

Now we can access two regions using one token.

Exp. 2:

Assume: Keystone DB are the same for all the user information. action: fernet key rotation in region 1

root@controller:~# keystone-manage fernet_rotate --keystone-user keystone --keystone-group keystone
2016-05-27 09:55:09.757 1791 WARNING keystone.token.providers.fernet.utils [-] [fernet_tokens] key_repository is world readable: /etc/keystone/fernet-keys/
2016-05-27 09:55:09.759 1791 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 3 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/1', '/etc/keystone/fernet-keys/2']
2016-05-27 09:55:09.759 1791 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 2
2016-05-27 09:55:09.759 1791 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 3
2016-05-27 09:55:09.760 1791 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 3
2016-05-27 09:55:09.761 1791 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2016-05-27 09:55:09.762 1791 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: [1]

Now the key repostory of region1 and region 2 is different.

Again we use the same Token to access region1 and region2.

Accessing Region1

junmein$ bash getTenant.sh
{"tenants_links": [], "tenants": [{"description": "atesttenant", "enabled": true, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}]}junmeindeMacBook-Pro:openstack_api

Accessing Region2

junmein$ bash getTenant2.sh
{"tenants_links": [], "tenants": [{"description": "atesttenant", "enabled": true, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}]}junmeindeMacBook-Pro:openstack_api

That is satify the fernet key rotation algorithm that both of region can be access even using the token generated before key rotation.

Exp. 3

Assume: Keystone DB are the same for all the user information. action: fernet key rotation in region 1 again.

keystone-manage fernet_rotate --keystone-user keystone --keystone-group keystone
2016-05-27 09:59:43.946 1806 WARNING keystone.token.providers.fernet.utils [-] [fernet_tokens] key_repository is world readable: /etc/keystone/fernet-keys/
2016-05-27 09:59:43.948 1806 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 3 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/2', '/etc/keystone/fernet-keys/3']
2016-05-27 09:59:43.949 1806 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 3
2016-05-27 09:59:43.949 1806 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 4
2016-05-27 09:59:43.950 1806 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 4
2016-05-27 09:59:43.951 1806 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2016-05-27 09:59:43.952 1806 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: [2]

In Region1's key repository

root@controller:~# ls /etc/keystone/fernet-keys/
0  3  4

Our Token is generated by using index 1. Now 1 has been purged after key rotation. So we cannot access to Region1. The Token has been expired.

bash getTenant.sh
{"error": {"message": "The request you have made requires authentication.", "code": 401, "title": "Unauthorized"}}

But in Region2 index 1 key is still in the key repository, so we can connect to Region.

root@controller2:~# ls /etc/keystone/fernet-keys/
0  1  2
bash getTenant2.sh
{"tenants_links": [], "tenants": [{"description": "atesttenant", "enabled": true, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}]}junmeindeMacBook-Pro:openstack_api

Conclusion for Multi-Rgions with Keystone Fernet Key

  1. Syn user data by DB replication is necessary, since we need to keep all the user information are the same.
  2. We don't need to sync token, very heavy, that will help the system more stable.
  3. Using same token to multiple regions is validate with both same token, and same key repository.
  4. After rotate, assume region 1, two region can still works using the same token, and new token. Since encryption key still in the keys(for decryption) of repository for both region.
  5. Sync key repository are necessary, but not instantly, we can do it before rotate twice.
  6. After rotate twice, if region2 lost sync twice, some key will be purged and some token will be expired. And some token will be invalidate in region2 or region1, since encryption key is not existed in both key repository(for decrption).

The Process is to multiple region is

  1. Replicate keystone DB, including all users information.
  2. sync initial key repository, to make sure both regions are the same.
  3. rotate
  4. sync key repository (No need to instantly).

Keystone: An unexpected error prevented the server from fulfilling your request

junmeindeMacBook-Pro:openstack_api junmein$ bash getToken.sh
HTTP/1.1 500 Internal Server Error
Vary: X-Auth-Token
Content-Type: application/json
Content-Length: 143
X-Openstack-Request-Id: req-33be785e-354b-43cf-b8bb-42a1cfaa7399
Date: Thu, 26 May 2016 12:10:47 GMT

{"error": {"message": "An unexpected error prevented the server from fulfilling your request.", "code": 500, "title": "Internal Server Error"}}junmeindeMacBook-Pro:openstack_api junmein$





How to solve it.

Increase CPU to 2 and Memory to 2048MB

And Then,







junmeindeMacBook-Pro:openstack_api junmein$ bash getToken.sh
HTTP/1.1 201 Created
X-Subject-Token: 8ff2f1ac3e90408f9e07740c899fbec7
Vary: X-Auth-Token
Content-Type: application/json
Content-Length: 1025
X-Openstack-Request-Id: req-38d18987-1bd2-4741-8076-71537aa9f8b0
Date: Thu, 26 May 2016 12:11:42 GMT

{"token": {"methods": ["password"], "roles": [{"id": "b4a892c14ba544cc84512002fa53bc97", "name": "admin"}], "expires_at": "2016-05-26T13:11:42.427621Z", "project": {"domain": {"id": "default", "name": "Default"}, "id": "ab202654a6734d0b8099eec2d49fd27b", "name": "admin"}, "catalog": [{"endpoints": [{"region_id": "RegionTwo", "url": "http://lala/v2", "region": "RegionTwo", "interface": "admin", "id": "903f2a276e40478987e8a2fa260602c4"}, {"region_id": "RegionTwo", "url": "http://haha/v2", "region": "RegionTwo", "interface": "public", "id": "c1fc46cd6ace40bea248e49d8dba24ac"}, {"region_id": "RegionTwo", "url": "http://sasa/v2", "region": "RegionTwo", "interface": "internal", "id": "dbf08bfe4490403aa70fb49e33590a82"}], "type": "service1", "id": "0204f666bfc24c9aa9f5922ce6c349b1", "name": "service1"}], "extras": {}, "user": {"domain": {"id": "default", "name": "Default"}, "id": "610de8e29c654110aab95a69f95991cd", "name": "admin"}, "audit_ids": ["r412AaXpSxSwL0mqZZrYMg"], "issued_at": "2016-05-26T12:11:42.427661Z"}}

Keystone (Kilo) Installation from Source Code

keystonesource

Keystone kilo installation by source code

Thanks for this Blog,

https://developer.rackspace.com/blog/install-openstack-from-source/

But, as usual, the procedure is not simple like that since it always failure if we just follow the steps. So we modify the procedure and post it on my Blog.

installation

apt-get install -y mysql-server
sed -i "s/127.0.0.1/$MY_PRIVATE_IP\nskip-name-resolve\ncharacter-set-server = utf8\ncollation-server = utf8_general_ci\ninit-connect = 'SET NAMES utf8'/g" /etc/mysql/my.cnf

restart mysql
mysql  -u root -pmysql -e "create database keystone;"
mysql  -u root -pmysql -e "GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone';"
mysql  -u root -pmysql -e "GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone';"
apt-get install -y python-dev libmysqlclient-dev libffi-dev libssl-dev
pip install python-glanceclient python-keystoneclient python-openstackclient
pip install repoze.lru pbr mysql-python
useradd --home-dir "/var/lib/keystone" \
    --create-home \
    --system \
    --shell /bin/false \
    keystone

#Create essential dirs

mkdir -p /var/log/keystone
mkdir -p /etc/keystone

#Set ownership of the dirs

chown -R keystone:keystone /var/log/keystone
chown -R keystone:keystone /var/lib/keystone
chown keystone:keystone /etc/keystone
git clone https://github.com/openstack/keystone.git -b stable/kilo
cp -R keystone/etc/* /etc/keystone/
cd keystone
python setup.py install
pip install -r requirements.txt
cp /etc/keystone/keystone.conf.sample /etc/keystone/keystone.conf
sed -i "s|database]|database]\nconnection = mysql://keystone:keystone@$MY_IP/keystone|g" /etc/keystone/keystone.conf
sed -i 's/#admin_token = ADMIN/admin_token = SuperSecreteKeystoneToken/g' /etc/keystone/keystone.conf

edit token section as followed.

[token]

provider = keystone.token.providers.uuid.Provider
driver = keystone.token.persistence.backends.sql.Token

apt-get install python-mysqldb keystone-manage db_sync

Excute the folloing script, the keystone then started. ``` cat > /etc/init/keystone.conf << EOF description "Keystone API server" author "Soren Hansen soren@linux2go.dk"

start on runlevel [2345] stop on runlevel [!2345]

respawn

exec start-stop-daemon --start --chuid keystone --chdir /var/lib/keystone --name keystone --exec /usr/local/bin/keystone-all -- --config-file=/etc/keystone/keystone.conf --log-file=/var/log/keystone/keystone.log EOF

start keystone ```

Now you can use the following CLI to start/stop keystone.

service keystone start
service keystone stop

check keystone daemon

root@keystonesource:~# ps aux|grep keystone
keystone  17495  2.3  6.1 171316 62004 ?        Ss   18:24   0:07 /usr/bin/python /usr/local/bin/keystone-all --config-file=/etc/keystone/keystone.conf --log-file=/var/log/keystone/keystone.log
keystone  17625  0.0  6.8 275796 68964 ?        S    18:24   0:00 /usr/bin/python /usr/local/bin/keystone-all --config-file=/etc/keystone/keystone.conf --log-file=/var/log/keystone/keystone.log
keystone  17626  0.0  6.8 276024 68956 ?        S    18:24   0:00 /usr/bin/python /usr/local/bin/keystone-all --config-file=/etc/keystone/keystone.conf --log-file=/var/log/keystone/keystone.log
keystone  17627  0.0  5.8 171316 58228 ?        S    18:24   0:00 /usr/bin/python /usr/local/bin/keystone-all --config-file=/etc/keystone/keystone.conf --log-file=/var/log/keystone/keystone.log
keystone  17628  0.0  5.8 171316 58228 ?        S    18:24   0:00 /usr/bin/python /usr/local/bin/keystone-all --config-file=/etc/keystone/keystone.conf --log-file=/var/log/keystone/keystone.log
root      17721  0.0  0.2  10468  2136 pts/0    S+   18:30   0:00 grep --color=auto keystone

access keystone

before we start to access keystone we might setup linux enviroment.

install keystone client

apt-get install python-keystoneclient

edit openrc_admin

export OS_SERVICE_TOKEN=tokentoken
export OS_SERVICE_ENDPOINT=http://localhost:35357/v2.0

execute the environment

source openrc_admin

Then you can start to use keystone by CLI. If you are familar with restful api, you don't need to set up linux environment.

keystone --debug tenant-create --name admin --description "Admin Tenant"

keystone --debug tenant-list

adding Endpoint

root@keystonesource:~# keystone service-create --name service1 --type service1
/usr/local/lib/python2.7/dist-packages/keystoneclient/shell.py:65: DeprecationWarning: The keystone CLI is deprecated in favor of python-openstackclient. For a Python library, continue using python-keystoneclient.
  'python-keystoneclient.', DeprecationWarning)
+-------------+----------------------------------+
|   Property  |              Value               |
+-------------+----------------------------------+
| description |                                  |
|   enabled   |               True               |
|      id     | 0204f666bfc24c9aa9f5922ce6c349b1 |
|     name    |             service1             |
|     type    |             service1             |
+-------------+----------------------------------+
root@keystonesource:~# keystone endpoint-create --region RegionTwo --service-id 0204f666bfc24c9aa9f5922ce6c349b1 --publicurl http://haha/v2 --adminurl http://lala/v2 --internalurl http://sasa/v2
/usr/local/lib/python2.7/dist-packages/keystoneclient/shell.py:65: DeprecationWarning: The keystone CLI is deprecated in favor of python-openstackclient. For a Python library, continue using python-keystoneclient.
  'python-keystoneclient.', DeprecationWarning)
+-------------+----------------------------------+
|   Property  |              Value               |
+-------------+----------------------------------+
|   adminurl  |          http://lala/v2          |
|      id     | b7885c5ee730459aa464ef0cf8965794 |
| internalurl |          http://sasa/v2          |
|  publicurl  |          http://haha/v2          |
|    region   |            RegionTwo             |
|  service_id | 0204f666bfc24c9aa9f5922ce6c349b1 |
+-------------+----------------------------------+

some error

/usr/local/lib/python2.7/dist-packages/keystoneclient/shell.py:65: DeprecationWarning: The keystone CLI is deprecated in favor of python-openstackclient. For a Python library, continue using python-keystoneclient.
  'python-keystoneclient.', DeprecationWarning)

show region endpoint


root@keystonesource:~# keystone --debug endpoint-list
/usr/local/lib/python2.7/dist-packages/keystoneclient/shell.py:65: DeprecationWarning: The keystone CLI is deprecated in favor of python-openstackclient. For a Python library, continue using python-keystoneclient.
  'python-keystoneclient.', DeprecationWarning)
DEBUG:keystoneclient.session:REQ: curl -g -i -X GET http://localhost:35357/v2.0/endpoints -H "User-Agent: python-keystoneclient" -H "Accept: application/json" -H "X-Auth-Token: {SHA1}c6af3a86ea7fe57895754929accbf9869643da36"
INFO:urllib3.connectionpool:Starting new HTTP connection (1): localhost
DEBUG:urllib3.connectionpool:Setting read timeout to 600.0
DEBUG:urllib3.connectionpool:"GET /v2.0/endpoints HTTP/1.1" 200 243
DEBUG:keystoneclient.session:RESP: [200] date: Thu, 12 May 2016 10:29:01 GMT content-type: application/json content-length: 243 vary: X-Auth-Token x-openstack-request-id: req-1b0a5879-242f-4f49-b002-9558e04e1d2b
RESP BODY: {"endpoints": [{"internalurl": "http://sasa/v2", "adminurl": "http://lala/v2", "service_id": "0204f666bfc24c9aa9f5922ce6c349b1", "region": "RegionTwo", "enabled": true, "id": "b7885c5ee730459aa464ef0cf8965794", "publicurl": "http://haha/v2"}]}

+----------------------------------+-----------+----------------+----------------+----------------+----------------------------------+
|                id                |   region  |   publicurl    |  internalurl   |    adminurl    |            service_id            |
+----------------------------------+-----------+----------------+----------------+----------------+----------------------------------+
| b7885c5ee730459aa464ef0cf8965794 | RegionTwo | http://haha/v2 | http://sasa/v2 | http://lala/v2 | 0204f666bfc24c9aa9f5922ce6c349b1 |
+----------------------------------+-----------+----------------+----------------+----------------+----------------------------------+

--debug mode的情況下會顯示restapi的使用方式,但上述的token是假的,token可由自己設定的admin token去執行。

show endpoint curl -g -i -X GET http://localhost:35357/v2.0/endpoints -H "Accept: application/json" -H "X-Auth-Token: tokentoken"

show tenant list

keystone --debug tenant-list

curl -g -i -X GET http://localhost:35357/v2.0/tenants -H "User-Agent: python-keystoneclient" -H "Accept: application/json" -H "X-Auth-Token: {SHA1}c6af3a86ea7fe57895754929accbf9869643da36"

create tenant

keystone --debug tenant-create --name=demo

curl -g -i -X POST http://localhost:35357/v2.0/tenants -H "User-Agent: python-keystoneclient" -H "Content-Type: application/json" -H "Accept: application/json" -H "X-Auth-Token: {SHA1}c6af3a86ea7fe57895754929accbf9869643da36" -d '{"tenant": {"enabled": true, "name": "demo", "description": null}}'

Keystone: Setup Fernet Token and Analysis Source Code

keystonefernet

Introduce Keystone Fernet Token

We want to introduce how to setup Fernet token in Keystone, and trace keystone source code (kilo) to understand how get token and validate token works.

Installation

Assume you have installed OpenStack or Keystone already. You can see my Blog How to install Keystone.

http://gogosatellite.blogspot.tw/2016/03/keystone-domain-endpoint.html

Then, you can change the setting based on a normal deployment of Keystone.

edit openrc

export OS_TOKEN=iamadmin
export OS_SERVICE_ENDPOINT=http://controller:35357/v2.0

Set up Environment

source openrc
mkdir /etc/keystone/fernet-keys/
chown -R keystone:keystone /etc/keystone/fernet-keys
keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone

edit /etc/keystone/keystone.conf

[token]
provider = keystone.token.providers.fernet.Provider
#provider = keystone.token.providers.uuid.Provider
driver = keystone.token.persistence.backends.sql.Token

We use fernet token instead of uuid shown as above. We keeyp the setting of driver but it takes no effect.

Restart keystone service

service apache2 restart

to get token from Keystone.

curl -si -d @token-request2.json -H "Content-type: application/json" http://172.16.235.128:35357/v3/auth/tokens

where token-request2.json

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "domain": {
                        "name": "Default"
                    },
                    "name": "newuser",
                    "password": "newuser"
                }
            }
        },
        "scope": {
            "project": {
                "domain": {
                    "name": "Default"
                },
                "name": "testtenant"
            }
        }
    }
}

Result

You can see the X-Subject-Token, we got the Fernet Token. Where getToken.sh is just a curl command we discussed above.

junmeindeMacBook-Pro:openstack_api junmein$ bash getToken.sh
HTTP/1.1 201 Created
Date: Thu, 26 May 2016 11:10:16 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Subject-Token: gAAAAABXRtmaD0t0r3n0suqWsEOu4Yp76JaLgOJYLdzRJDNx0FwTb5oBVn429dMmMhiQFVGXltQVvlrhaVBaB9SkYnuTqxq06LnjoHkTmZd-afF5fP-g0YX9n2pG6ESNFgNepgmQANQqEl0oVBrC_S8SrKP9f8QcJkuxBDSwQfKIi3mylbvugK4%3D
Vary: X-Auth-Token
X-Distribution: Ubuntu
x-openstack-request-id: req-58735ed6-aeed-4fcb-99b2-bf03512fc45f
Content-Length: 2065
Content-Type: application/json

{"token": {"methods": ["password"], "roles": [{"id": "9fe2ff9ee4384b1894a90878d3e92bab", "name": "_member_"}], "expires_at": "2016-05-26T12:10:17.283270Z", "project": {"domain": {"id": "default", "name": "Default"}, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}, "catalog": [{"endpoints": [{"region_id": "RegionOne", "url": "http://controller:35357/v2.0", "region": "RegionOne", "interface": "admin", "id": "02f744638b2f44

To make sure the token table in the database won't be increased, since fernet token is not persistent.

mysql -uroot -pshark -e 'use keystone; select * from token;'|wc -l

How to Create Token using Fernet Token Algorithm

def create_token(self, user_id, expires_at, audit_ids, methods=None,
                 domain_id=None, project_id=None, trust_id=None,
                 federated_info=None):
    """Given a set of payload attributes, generate a Fernet token."""
    
    .
    .
    elif project_id:
    version = ProjectScopedPayload.version
    payload = ProjectScopedPayload.assemble(
        user_id,
        methods,
        project_id,
        expires_at,
        audit_ids)
        
        
class ProjectScopedPayload(BasePayload):
    version = 2

    @classmethod
    def assemble(cls, user_id, methods, project_id, expires_at, audit_ids):
        """Assemble the payload of a project-scoped token.

        :param user_id: ID of the user in the token request
        :param methods: list of authentication methods used
        :param project_id: ID of the project to scope to
        :param expires_at: datetime of the token's expiration
        :param audit_ids: list of the token's audit IDs
        :returns: the payload of a project-scoped token

        """
        b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
        methods = auth_plugins.convert_method_list_to_integer(methods)
        b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
        expires_at_int = cls._convert_time_string_to_int(expires_at)
        b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
                           audit_ids))
        return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids)    

So the paidload is a list that one can access it by using paidload[0]. It's quite a beautiful skill that we will discuss it later.

and then it will form the token as follows.

versioned_payload = (version,) + payload
serialized_payload = msgpack.packb(versioned_payload)
token = self.pack(serialized_payload)

return token

so the serial encryption packs are

  • version
  • user_id
  • method
  • project_id
  • expire_at
  • audit_ids

We can see what b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) doing.

@classmethod
def convert_uuid_hex_to_bytes(cls, uuid_string):
    """Compress UUID formatted strings to bytes.

    :param uuid_string: uuid string to compress to bytes
    :returns: a byte representation of the uuid

    """
    # TODO(lbragstad): Wrap this in an exception. Not sure what the case
    # would be where we couldn't handle what we've been given but incase
    # the integrity of the token has been compromised.
    uuid_obj = uuid.UUID(uuid_string)
    return uuid_obj.bytes

Just ,transfer uuid hex to bytes or int。

What is Serialize Paidload !! Godd Stuff!!

It's a beautiful way to pack a accessable attibute, list, to a string and send it to socket or mq. And we then unpack it and access it as a list directly. That's msgpack doing.

This is an example, to realize what is msgpack doing. This is called serialize paidload.

ori = ('ddd', 'aaa', 'bbb', 'ccc')
pack = ��ddd�aaa�bbb�ccc 
unpack = ['ddd', 'aaa', 'bbb', 'ccc'] 

In keystone we pack it and encrypt it. After decrypt it we can access a list directly.

encrypt key

def crypto(self):
    """Return a cryptography instance.

    You can extend this class with a custom crypto @property to provide
    your own token encoding / decoding. For example, using a different
    cryptography library (e.g. ``python-keyczar``) or to meet arbitrary
    security requirements.

    This @property just needs to return an object that implements
    ``encrypt(plaintext)`` and ``decrypt(ciphertext)``.

    """
    keys = utils.load_keys()
  .
  .
  .

that contains primary, secondary, ..... and rotation. and finally encrypt it by pack function.

def load_keys():
    """Load keys from disk into a list.

    The first key in the list is the primary key used for encryption. All
    other keys are active secondary keys that can be used for decrypting
    tokens.

    """
    if not validate_key_repository():
        return []

    # build a dictionary of key_number:encryption_key pairs
    keys = dict()
    for filename in os.listdir(CONF.fernet_tokens.key_repository):
        path = os.path.join(CONF.fernet_tokens.key_repository, str(filename))
        if os.path.isfile(path):
            with open(path, 'r') as key_file:
                try:
                    key_id = int(filename)
                except ValueError:
                    pass
                else:
                    keys[key_id] = key_file.read()

    if len(keys) != CONF.fernet_tokens.max_active_keys:
        # If there haven't been enough key rotations to reach max_active_keys,
        # or if the configured value of max_active_keys has changed since the
        # last rotation, then reporting the discrepancy might be useful. Once
        # the number of keys matches max_active_keys, this log entry is too
        # repetitive to be useful.
        LOG.info(_LI(
            'Loaded %(count)s encryption keys from: %(dir)s'), {
                'count': len(keys),
                'dir': CONF.fernet_tokens.key_repository})

    # return the encryption_keys, sorted by key number, descending
    return [keys[x] for x in sorted(keys.keys(), reverse=True)]

Now we want to solve the fernet key problem.
Before obtaining the fernet token service, we must setup by keystone-manage. As the steps we done before.

 mkdir /etc/keystone/fernet-keys/
$ keystone-manage fernet_setup

and the configuration

[fernet_tokens]
        # key repository where the fernet keys are stored
    key_repository = /etc/keystone/fernet-keys/
        # maximum number of keys in key repository
    max_active_keys =  # default is 3
$ ls /etc/keystone/fernet-keys
0 1 2 3 4

An example for fernet.MultiFernet

https://cryptography.io/en/latest/fernet/
https://media.readthedocs.org/pdf/cryptography/latest/cryptography.pdf

>>> from cryptography.fernet import Fernet, MultiFernet
>>> key1 = Fernet(Fernet.generate_key())
>>> key2 = Fernet(Fernet.generate_key())
>>> f = MultiFernet([key1, key2])
>>> token = f.encrypt(b"Secret message!")
.
.
>>> f.decrypt(token)
'Secret message!'

After load serveral keys, and pass to fernet encrypt function to get encrpt result of token. and where the key we get is from the directory of /etc/keystone/fernet-keys/.

Key Rotation

What is MultiFernet doing, according to the offcial site's explanation of MultiFernet:

MultiFernet performs all encryption options using the first key in the list provided. MultiFernet attempts to decrypt tokens with each key in turn. A cryptography.fernet.InvalidToken exception is raised if the correct key is not found in the list provided. Key rotation makes it easy to replace old keys. You can add your new key at the front of the list to start encrypting new messages, and remove old keys as they are no longer needed.

It will use the first key to encrypt the data. Then decrypt by each key in turn, since the key will rotate for security.

where is the key location

root@controller:~# ls /etc/keystone/fernet-keys/
0  1

to see content of the key

root@controller:~# cat /etc/keystone/fernet-keys/0
BIoWWHPzDcoNRhFsg3TFzrRHoYVlL1MECDreHBREqJo=

Conclusion: Generate Token and Validate Token

使用project based Fernet Token包含以下物件: Fernet algorithsm uses the bellow parameters.

  • version
  • user_id
  • method
  • project_id
  • expire_at
  • audit_ids

The process of getting a token:

transfer the above hex string object to **bytes** or **int** -> to form a list -> serialize pack(msgpack.pack) -> Rotated MutiFernet Encryption (Fernet.MultiFernet(key1, key2..)) -> Token.

The process of validating a token (inverse process):

Token -> Rotated MultiFernet Decryption -> serialize unpack(msgpack.unpack) -> list -> access list -> bytes int object transfers to  hex string -> to check expire time .. -> validate or invalidate token.

Now we realize how is the keystone to get token and validate token.

And next chapter, we want to talk more about validate token.

How To Validate Token

def validate_token(self, token):
    """Validates a Fernet token and returns the payload attributes."""
    # Convert v2 unicode token to a string
    if not isinstance(token, six.binary_type):
        token = token.encode('ascii')
    
    serialized_payload = self.unpack(token)
    versioned_payload = msgpack.unpackb(serialized_payload)
    version, payload = versioned_payload[0], versioned_payload[1:]
    
    # depending on the formatter, these may or may not be defined
    domain_id = None
    project_id = None
    trust_id = None
    federated_info = None
    
    if version == UnscopedPayload.version:
        (user_id, methods, expires_at, audit_ids) = (
            UnscopedPayload.disassemble(payload))
    elif version == DomainScopedPayload.version:
        (user_id, methods, domain_id, expires_at, audit_ids) = (
            DomainScopedPayload.disassemble(payload))
    elif version == ProjectScopedPayload.version:
        (user_id, methods, project_id, expires_at, audit_ids) = (
            ProjectScopedPayload.disassemble(payload))
    elif version == TrustScopedPayload.version:
        (user_id, methods, project_id, expires_at, audit_ids, trust_id) = (
            TrustScopedPayload.disassemble(payload))
    elif version == FederatedPayload.version:
        (user_id, methods, expires_at, audit_ids, federated_info) = (
            FederatedPayload.disassemble(payload))
    else:
        # If the token_format is not recognized, raise ValidationError.
        raise exception.ValidationError(_(
            'This is not a recognized Fernet payload version: %s') %
            version)
    # rather than appearing in the payload, the creation time is encoded
    # into the token format itself
    created_at = TokenFormatter.creation_time(token)
    created_at = timeutils.isotime(at=created_at, subsecond=True)
    expires_at = timeutils.parse_isotime(expires_at)
    expires_at = timeutils.isotime(at=expires_at, subsecond=True)
    
    return (user_id, methods, audit_ids, domain_id, project_id, trust_id,
            federated_info, created_at, expires_at)
def disassemble(cls, payload):
    """Disassemble a payload into the component data.

    :param payload: the payload of a token
    :return: a tuple containing the user_id, auth methods, project_id,
             expires_at_str, and audit_ids

    """
    (is_stored_as_bytes, user_id) = payload[0]
    if is_stored_as_bytes:
        user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id)
    methods = auth_plugins.convert_integer_to_method_list(payload[1])
    (is_stored_as_bytes, project_id) = payload[2]
    if is_stored_as_bytes:
        project_id = cls.attempt_convert_uuid_bytes_to_hex(project_id)
    expires_at_str = cls._convert_int_to_time_string(payload[3])
    audit_ids = list(map(provider.base64_encode, payload[4]))

    return (user_id, methods, project_id, expires_at_str, audit_ids)

The process is very similar to generate Token.