From 0ac90c5522145644a78ecbc483cf183a754a4bc1 Mon Sep 17 00:00:00 2001 From: Lin Yang Date: Fri, 7 Oct 2016 08:36:35 +0800 Subject: [PATCH] Initial import from external repository External repo: https://github.com/mganguli/RSC Commit: 49199a82045f1d6f231eb477de3dbcd59492e9d9 Change-Id: I9eaec387605a39ba5e4c571026cacb1845938231 --- .gitignore | 70 +++ CONTRIBUTING.rst | 17 + HACKING.rst | 4 + MANIFEST.in | 7 + README.rst | 91 ++++ babel.cfg | 1 + doc/README.md | 3 + doc/api-mockup/index.json | 30 ++ doc/api-mockup/v1/flavors/criteria/index.json | 33 ++ doc/api-mockup/v1/flavors/index.json | 39 ++ doc/api-mockup/v1/flavors/post.json | 17 + doc/api-mockup/v1/index.json | 50 +++ .../index.json | 53 +++ .../storages/index.json | 16 + .../index.json | 52 +++ doc/api-mockup/v1/nodes/index.json | 34 ++ .../index.json | 11 + .../index.json | 11 + doc/api-mockup/v1/storages/index.json | 34 ++ doc/api/VALENCE_API-v0.4.1.docx | Bin 0 -> 73113 bytes doc/source/conf.py | 75 ++++ doc/source/contributing.rst | 4 + doc/source/index.rst | 24 + doc/source/init/valence-api.conf | 15 + doc/source/init/valence-controller.conf | 14 + doc/source/installation.rst | 12 + doc/source/readme.rst | 1 + doc/source/usage.rst | 7 + doc/ui-proxy/apache/README.md | 71 +++ doc/ui-proxy/apache/podm-proxy.conf | 49 ++ etc/valence/valence.conf.sample | 40 ++ install_valence.sh | 42 ++ releasenotes/notes/.placeholder | 0 releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 272 ++++++++++++ releasenotes/source/index.rst | 8 + releasenotes/source/unreleased.rst | 5 + requirements.txt | 41 ++ setup.cfg | 59 +++ setup.py | 29 ++ test-requirements.txt | 17 + tox.ini | 64 +++ valence/__init__.py | 0 valence/api/__init__.py | 0 valence/api/app.py | 61 +++ valence/api/config.py | 66 +++ valence/api/controllers/__init__.py | 0 valence/api/controllers/base.py | 35 ++ valence/api/controllers/link.py | 56 +++ valence/api/controllers/root.py | 78 ++++ valence/api/controllers/types.py | 132 ++++++ valence/api/controllers/v1/__init__.py | 0 valence/api/controllers/v1/controller.py | 84 ++++ valence/api/controllers/v1/flavor.py | 44 ++ valence/api/controllers/v1/nodes.py | 84 ++++ valence/api/controllers/v1/storages.py | 44 ++ valence/api/hooks.py | 13 + valence/cmd/__init__.py | 0 valence/cmd/api.py | 49 ++ valence/cmd/controller.py | 56 +++ valence/common/__init__.py | 0 valence/common/context.py | 75 ++++ valence/common/exceptions.py | 79 ++++ valence/common/osinterface.py | 97 ++++ valence/common/redfish/__init__.py | 0 valence/common/redfish/api.py | 417 ++++++++++++++++++ valence/common/redfish/config.py | 33 ++ valence/common/redfish/tree.py | 122 +++++ valence/common/rpc.py | 139 ++++++ valence/common/rpc_service.py | 89 ++++ valence/controller/__init__.py | 0 valence/controller/api.py | 67 +++ valence/controller/config.py | 65 +++ valence/controller/handlers/__init__.py | 0 .../controller/handlers/flavor_controller.py | 37 ++ .../controller/handlers/node_controller.py | 64 +++ valence/flavor/__init__.py | 0 valence/flavor/flavor.py | 54 +++ valence/flavor/generatorbase.py | 37 ++ valence/flavor/plugins/__init__.py | 5 + valence/flavor/plugins/assettag.py | 46 ++ valence/flavor/plugins/default.py | 43 ++ valence/flavor/plugins/example.py | 27 ++ valence/objects/__init__.py | 0 valence/objects/base.py | 63 +++ valence/tests/__init__.py | 24 + valence/tests/config.py | 37 ++ valence/tests/test_functional.py | 22 + valence/tests/test_units.py | 7 + valence/ui/README.md | 44 ++ valence/ui/package.json | 61 +++ valence/ui/src/customized.css | 163 +++++++ valence/ui/src/index.html | 16 + valence/ui/src/js/components/Layout.js | 144 ++++++ .../src/js/components/home/ComposeDisplay.js | 122 +++++ .../src/js/components/home/DetailDisplay.js | 17 + valence/ui/src/js/components/home/Home.js | 153 +++++++ valence/ui/src/js/components/home/NodeList.js | 86 ++++ .../ui/src/js/components/home/ResourceList.js | 30 ++ valence/ui/src/js/config.js | 19 + valence/ui/src/js/main.js | 6 + valence/ui/src/js/util.js | 143 ++++++ valence/ui/src/rsd.css | 27 ++ valence/ui/webpack.config.js | 33 ++ 105 files changed, 4937 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 doc/README.md create mode 100644 doc/api-mockup/index.json create mode 100644 doc/api-mockup/v1/flavors/criteria/index.json create mode 100644 doc/api-mockup/v1/flavors/index.json create mode 100644 doc/api-mockup/v1/flavors/post.json create mode 100644 doc/api-mockup/v1/index.json create mode 100644 doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/index.json create mode 100644 doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/storages/index.json create mode 100644 doc/api-mockup/v1/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0/index.json create mode 100644 doc/api-mockup/v1/nodes/index.json create mode 100644 doc/api-mockup/v1/storages/4c16a45b-b029-49c4-af84-1abcf458a062/index.json create mode 100644 doc/api-mockup/v1/storages/bbfddf09-4d7e-40d5-88a9-8acfb2f88c21/index.json create mode 100644 doc/api-mockup/v1/storages/index.json create mode 100644 doc/api/VALENCE_API-v0.4.1.docx create mode 100644 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/init/valence-api.conf create mode 100755 doc/source/init/valence-controller.conf create mode 100644 doc/source/installation.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/usage.rst create mode 100644 doc/ui-proxy/apache/README.md create mode 100644 doc/ui-proxy/apache/podm-proxy.conf create mode 100644 etc/valence/valence.conf.sample create mode 100755 install_valence.sh create mode 100644 releasenotes/notes/.placeholder create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/unreleased.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini create mode 100644 valence/__init__.py create mode 100644 valence/api/__init__.py create mode 100644 valence/api/app.py create mode 100644 valence/api/config.py create mode 100644 valence/api/controllers/__init__.py create mode 100644 valence/api/controllers/base.py create mode 100644 valence/api/controllers/link.py create mode 100644 valence/api/controllers/root.py create mode 100644 valence/api/controllers/types.py create mode 100644 valence/api/controllers/v1/__init__.py create mode 100644 valence/api/controllers/v1/controller.py create mode 100644 valence/api/controllers/v1/flavor.py create mode 100644 valence/api/controllers/v1/nodes.py create mode 100644 valence/api/controllers/v1/storages.py create mode 100644 valence/api/hooks.py create mode 100644 valence/cmd/__init__.py create mode 100755 valence/cmd/api.py create mode 100644 valence/cmd/controller.py create mode 100644 valence/common/__init__.py create mode 100644 valence/common/context.py create mode 100644 valence/common/exceptions.py create mode 100644 valence/common/osinterface.py create mode 100644 valence/common/redfish/__init__.py create mode 100644 valence/common/redfish/api.py create mode 100644 valence/common/redfish/config.py create mode 100644 valence/common/redfish/tree.py create mode 100644 valence/common/rpc.py create mode 100644 valence/common/rpc_service.py create mode 100644 valence/controller/__init__.py create mode 100644 valence/controller/api.py create mode 100644 valence/controller/config.py create mode 100644 valence/controller/handlers/__init__.py create mode 100644 valence/controller/handlers/flavor_controller.py create mode 100644 valence/controller/handlers/node_controller.py create mode 100644 valence/flavor/__init__.py create mode 100644 valence/flavor/flavor.py create mode 100644 valence/flavor/generatorbase.py create mode 100644 valence/flavor/plugins/__init__.py create mode 100644 valence/flavor/plugins/assettag.py create mode 100644 valence/flavor/plugins/default.py create mode 100644 valence/flavor/plugins/example.py create mode 100644 valence/objects/__init__.py create mode 100644 valence/objects/base.py create mode 100644 valence/tests/__init__.py create mode 100644 valence/tests/config.py create mode 100644 valence/tests/test_functional.py create mode 100644 valence/tests/test_units.py create mode 100644 valence/ui/README.md create mode 100644 valence/ui/package.json create mode 100644 valence/ui/src/customized.css create mode 100644 valence/ui/src/index.html create mode 100644 valence/ui/src/js/components/Layout.js create mode 100644 valence/ui/src/js/components/home/ComposeDisplay.js create mode 100644 valence/ui/src/js/components/home/DetailDisplay.js create mode 100644 valence/ui/src/js/components/home/Home.js create mode 100644 valence/ui/src/js/components/home/NodeList.js create mode 100644 valence/ui/src/js/components/home/ResourceList.js create mode 100644 valence/ui/src/js/config.js create mode 100644 valence/ui/src/js/main.js create mode 100644 valence/ui/src/js/util.js create mode 100644 valence/ui/src/rsd.css create mode 100644 valence/ui/webpack.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39ad3ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg* +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +cover +.tox +nosetests.xml +.testrepository +.venv + +# Functional test +functional-tests.log +functional_creds.conf + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject +.idea + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp +.*sw? +*.DS_Store + +# generated config file +etc/magnum/magnum.conf.sample + +# Files created by releasenotes build +releasenotes/build + +# UI Node files +valence/ui/node_modules +valence/ui/npm-debug.log diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..82f15de --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/plasma diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..28c9fc6 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +plasma Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2e56eaa --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc +#recursive-include public * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..93028f8 --- /dev/null +++ b/README.rst @@ -0,0 +1,91 @@ +========================= +Openstack Valence Project +========================= + +Valence is a service for lifecycle management of pooled bare-metal hardware infrastructure such as Intel(R) Rack Scale architecture which uses Redfish(TM) as one of the management protocols. + +:Free software: Apache license +:Wiki: https://wiki.openstack.org/wiki/Valence +:Source: http://git.openstack.org/cgit/openstack/rsc +:Bugs: http://bugs.launchpad.net/openstack-valence + + +=========================== +Download and Installation +=========================== + +The following steps capture how to install valence. All installation steps require super user permissions. + +******************** +Valence installation +******************** + + 1. Install software dependencies + + ``$ sudo apt-get install git python-pip rabbitmq-server libyaml-0-2 python-dev`` + + 2. Configure RabbitMq Server + + ``$ sudo rabbitmqctl add_user rsd rsd #user this username/pwd in valence.conf`` + + ``$ sudo rabbitmqctl set_user_tags rsd administrator`` + + ``$ sudo rabbitmqctl set_permissions rsd ".*" ".*" ".*"`` + + 3. Clone the Valence code from git repo and change the directory to root Valence folder. + + 4. Install all necessary software pre-requisites using the pip requirements file. + + ``$ sudo -E pip install -r requirements.txt`` + + 5. Execute the 'install_valence.sh' file the Valence root directory. + + ``$ ./install_valence.sh`` + + 6. Check the values in valence.conf located at /etc/valence/valence.conf + + ``set the ip/credentials of podm for which this Valence will interact`` + + ``set the rabbitmq user/password to the one given above(Step 2)`` + + 7. Check the values in /etc/init/valence-api.conf, /etc/init/valence-controller.conf + + 8. Start api and controller services + + ``$ service valence-api start`` + + ``$ service valence-controller start`` + + 9. Logs are located at /var/logs/valence/ + +**************** +GUI installation +**************** +Please refer to the installation steps in the ui/README file. + + +********** +Components +********** + +Valence follows the typical OpenStack project setup. The components are listed below: + +valence-api +----------- +A pecan based daemon to expose Valence REST APIs. The api service communicates to the controller through AMQP. + +valence-controller +-------------- +The controller implements all the handlers for Plasma-api. It reads requests from the AMQP queue, process it and send the reponse back to the caller. + +valence-ui +-------- +valence-ui provides a GUI interface to invoke Valence APIs. + +========== +Features +========== +Please refer the Valence blueprints for supported and in-the-pipeline features. +``https://blueprints.launchpad.net/plasma`` + + diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..0d09177 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,3 @@ +RSC API spec and RSC mockup file. + + diff --git a/doc/api-mockup/index.json b/doc/api-mockup/index.json new file mode 100644 index 0000000..a459025 --- /dev/null +++ b/doc/api-mockup/index.json @@ -0,0 +1,30 @@ +{ + "name" : "OpenStack Plasma API", + "description" : "Plasma is an OpenStack project which aims to provide node composition based on redfish API.", + "default_version" : { + "status" : "CURRENT", + "version" : "1.1", + "links" : [ + { + "rel" : "self", + "href" : "http://openstack.example.com:8881/v1/" + } + ], + "id" : "v1", + "min_version" : "1.0" + }, + "versions" : [ + { + "status" : "CURRENT", + "links" : [ + { + "href" : "http://openstack.example.com:8881/v1/", + "rel" : "self" + } + ], + "id" : "v1", + "version" : "1.1", + "min_version" : "1.0" + } + ] +} diff --git a/doc/api-mockup/v1/flavors/criteria/index.json b/doc/api-mockup/v1/flavors/criteria/index.json new file mode 100644 index 0000000..41d8ee3 --- /dev/null +++ b/doc/api-mockup/v1/flavors/criteria/index.json @@ -0,0 +1,33 @@ +{ + "criteria" : [ + { + "id": "8f70656e-7374-6163-6b20-342065766222", + "links" : [ + { + "href": "http://openstack.example.com/v1/criteria/8f70656e-7374-6163-6b20-342065766222", + "rel" : "self" + }, + { + "href" : "http://openstack.example.com/criteria/8f70656e-7374-6163-6b20-342065766222", + "rel" : "bookmakr" + } + ], + "name" : "criteria 1" + }, + { + + "id": "8170656e-7374-6163-6b20-342065766112", + "links" : [ + { + "href": "http://openstack.example.com/v1/criteria/8170656e-7374-6163-6b20-342065766112", + "rel" : "self" + }, + { + "href" : "http://openstack.example.com/criteria/8170656e-7374-6163-6b20-342065766112", + "rel" : "bookmakr" + } + ], + "name" : "criteria 2" + } + ] +} diff --git a/doc/api-mockup/v1/flavors/index.json b/doc/api-mockup/v1/flavors/index.json new file mode 100644 index 0000000..6d3a3a6 --- /dev/null +++ b/doc/api-mockup/v1/flavors/index.json @@ -0,0 +1,39 @@ +{ + "flavors": [ + { + "id": "67730a1e-42b3-4813-8940-b961dcd0293c", + "links": [ + { + "href": "http://openstack.example.com/v1/flavors/67730a1e-42b3-4813-8940-b961dcd0293c", + "rel": "self" + }, + { + "href": "http://openstack.example.com/flavors/67730a1e-42b3-4813-8940-b961dcd0293c", + "rel": "bookmark" + } + ], + "name": "flavor1", + "criteria" : [ + {"id" : "8f70656e-7374-6163-6b20-342065766222"} + ] + }, + { + "id": "30abc156-d673-4e7c-bf2a-0a5098e14878", + "links": [ + { + "href": "http://openstack.example.com/v1/flavors/30abc156-d673-4e7c-bf2a-0a5098e14878", + "rel": "self" + }, + { + "href": "http://openstack.example.com/flavors/30abc156-d673-4e7c-bf2a-0a5098e14878", + "rel": "bookmark" + } + ], + "name": "flavor2", + "criteria" : [ + {"id" : "8f70656e-7374-6163-6b20-342065766222"}, + {"id" : "8170656e-7374-6163-6b20-342065766211"} + ] + } + ] +} diff --git a/doc/api-mockup/v1/flavors/post.json b/doc/api-mockup/v1/flavors/post.json new file mode 100644 index 0000000..e10c2dc --- /dev/null +++ b/doc/api-mockup/v1/flavors/post.json @@ -0,0 +1,17 @@ +{ + "flavors" : { + "criteria_id" : "8f70656e737461636b20342065766222", + "id" : "10", + "name" : "flavor 10", + "links": [ + { + "href": "http://openstack.example.com/v1/flavors/10", + "rel": "self" + }, + { + "href": "http://openstack.example.com/flavors/10", + "rel": "bookmark" + } + ] + } +} \ No newline at end of file diff --git a/doc/api-mockup/v1/index.json b/doc/api-mockup/v1/index.json new file mode 100644 index 0000000..612930e --- /dev/null +++ b/doc/api-mockup/v1/index.json @@ -0,0 +1,50 @@ +{ + "id" : "v1", + "links" : [ + { + "href" : "http://openstack.example.com:8881/v1/", + "rel" : "self" + }, + { + "rel" : "describedby", + "type" : "text/html", + "href" : "http://docs.openstack.org/developer/plasma/dev/api-spec-v1.html" + } + ], + "nodes" : [ + { + "rel" : "self", + "href" : "http://openstack.example.com:8881/v1/nodes/" + }, + { + "rel" : "bookmark", + "href" : "http://openstack.example.com:8881/nodes/" + } + ], + "storages" : [ + { + "href" : "http://openstack.example.com:8881/v1/storages/", + "rel" : "self" + }, + { + "rel" : "bookmark", + "href" : "http://openstack.example.com:8881/storages/" + } + ], + "flavors" : [ + { + "href" : "http://openstack.example.com:8881/v1/flavors/", + "rel" : "self" + }, + { + "rel" : "bookmark", + "href" : "http://openstack.example.com:8881/flavors/" + } + ], + "media_types" : [ + { + "type" : "application/vnd.openstack.plasma.v1+json", + "base" : "application/json" + } + ] +} diff --git a/doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/index.json b/doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/index.json new file mode 100644 index 0000000..90b2156 --- /dev/null +++ b/doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/index.json @@ -0,0 +1,53 @@ +{ + "node" : { + "id" : "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "ComposedNodeState" : "Off", + "boot_source" : "Localdisk", + "pending_boot_source" : "PXE", + "node_state" : "Allocated", + "health_status" : "OK", + "name" : null, + "pooling_group_id" : "11z23344-0099-7766-5544-33225511", + "metadata" : { + "nic" : [ + {"mac" : "f1:12:44:55:66:77"}, + {"mac" : "f2:44:44:44:44:88"} + ], + "mgmt_mac" : "00:AA:BB:CC:DD:EE", + "podid" : "POD1", + "rackid" : "Rack2", + "slotid" : "3", + "board_serialno" : "2M220100SL" + }, + "node_properties" : { + "cpu_arch" : "x86_64", + "cpu_count" : "2", + "memory_size_gb" : "32", + "network" : [ + { + "type" : "ethernet", + "speed" : "40000000" + } + ], + "memory_type" : "DDR4", + "storage" : [ + { + "type" : "SSD", + "volume_gb" : "40" + } + ] + }, + "created_at" : "2016-04-20T15:40:00+00:00", + "updated_at" : "2016-04-20T15:40:00+00:00", + "links": [ + { + "rel" : "self", + "href" : "https://openstack.example.com/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0" + }, + { + "rel" : "boomark", + "href" : "https://openstack.example.com/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0" + } + ] + } +} diff --git a/doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/storages/index.json b/doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/storages/index.json new file mode 100644 index 0000000..69e306d --- /dev/null +++ b/doc/api-mockup/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0/storages/index.json @@ -0,0 +1,16 @@ +{ + "storagevolumeAttachments": [ + { + "device": "/dev/sdd", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f803", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803" + }, + { + "device": "/dev/sdc", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804", + "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804" + } + ] +} diff --git a/doc/api-mockup/v1/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0/index.json b/doc/api-mockup/v1/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0/index.json new file mode 100644 index 0000000..2c0ce0f --- /dev/null +++ b/doc/api-mockup/v1/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0/index.json @@ -0,0 +1,52 @@ +{ + "node" : { + "id" : "ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0", + "nodestate" : "Off", + "boot_source" : "Localdisk", + "pending_boot_source" : "PXE", + "pooling_group_id" : "11z23344-0099-7766-5544-33225511", + "health_status" : "OK", + "name" : null, + "metadata" : { + "nic" : [ + {"mac" : "f1:12:44:55:66:77"}, + {"mac" : "f2:44:44:44:44:88"} + ], + "mgmt_mac" : "00:AA:BB:CC:DD:EE", + "podid" : "POD1", + "rackid" : "Rack2", + "slotid" : "3", + "board_serialno" : "2M220100SL" + }, + "node_properties" : { + "cpu_arch" : "x86_64", + "cpu_count" : "2", + "memory_size_gb" : "32", + "network" : [ + { + "type" : "ethernet", + "speed" : "40000000" + } + ], + "memory_type" : "DDR4", + "storage" : [ + { + "type" : "SSD", + "volume_gb" : "40" + } + ] + }, + "created_at" : "2016-04-20T15:40:00+00:00", + "updated_at" : "2016-04-20T15:40:00+00:00", + "links": [ + { + "rel" : "self", + "href" : "https://openstack.example.com/v1/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0" + }, + { + "rel" : "boomark", + "href" : "https://openstack.example.com/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0" + } + ] + } +} diff --git a/doc/api-mockup/v1/nodes/index.json b/doc/api-mockup/v1/nodes/index.json new file mode 100644 index 0000000..e236e3c --- /dev/null +++ b/doc/api-mockup/v1/nodes/index.json @@ -0,0 +1,34 @@ +{ + "nodes" : [ + { + "id" : "ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0", + "name" : "Server 1" , + "nodestate" : "PoweredOn" , + "links": [ + { + "rel" : "self", + "href" : "https://openstack.example.com/v1/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0" + }, + { + "href" : "https://openstack.example.com/nodes/ee1ecc3c-d3dd-f4ff-a6aa-uu7uk9k0", + "rel" : "bookmark" + } + ] + }, + { + "id" : "4d8c3732-a248-40ed-bebc-539a6ffd25c0" , + "name" : "Server 2", + "nodestate" : "PoweredOff" , + "links" : [ + { + "ref" : "self", + "href" : "https://openstack.example.com/v1/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0" + }, + { + "ref" : "bookmark", + "href" : "https://openstack.example.com/nodes/4d8c3732-a248-40ed-bebc-539a6ffd25c0" + } + ] + } + ] +} diff --git a/doc/api-mockup/v1/storages/4c16a45b-b029-49c4-af84-1abcf458a062/index.json b/doc/api-mockup/v1/storages/4c16a45b-b029-49c4-af84-1abcf458a062/index.json new file mode 100644 index 0000000..24bc1e3 --- /dev/null +++ b/doc/api-mockup/v1/storages/4c16a45b-b029-49c4-af84-1abcf458a062/index.json @@ -0,0 +1,11 @@ +{ + "storage_device" : + { + "deviceId" : "4c16a45b-b029-49c4-af84-1abcf458a062", + "pooling_group_id" : "11z23344-0099-7766-5544-33225511", + "health_status" : "critical", + "capacity_mb" : "1000", + "property_foo1" : "value_bar1", + "property_foo2" : "value_bar2" + } +} diff --git a/doc/api-mockup/v1/storages/bbfddf09-4d7e-40d5-88a9-8acfb2f88c21/index.json b/doc/api-mockup/v1/storages/bbfddf09-4d7e-40d5-88a9-8acfb2f88c21/index.json new file mode 100644 index 0000000..e3eb91d --- /dev/null +++ b/doc/api-mockup/v1/storages/bbfddf09-4d7e-40d5-88a9-8acfb2f88c21/index.json @@ -0,0 +1,11 @@ +{ + "storage_device" : + { + "deviceId" : "bbfddf09-4d7e-40d5-88a9-8acfb2f88c21", + "pooling_group_id" : "11z23344-0099-7766-5544-33225511", + "health_status" : "critical", + "capacity_mb" : "1000", + "property_foo1" : "value_bar1", + "property_foo2" : "value_bar2" + } +} diff --git a/doc/api-mockup/v1/storages/index.json b/doc/api-mockup/v1/storages/index.json new file mode 100644 index 0000000..6b47353 --- /dev/null +++ b/doc/api-mockup/v1/storages/index.json @@ -0,0 +1,34 @@ +{ + "storges" : [ + { + "deviceId" : "bbfddf09-4d7e-40d5-88a9-8acfb2f88c21", + "pooling_group_id" : "11z23344-0099-7766-5544-33225511", + "allocate_status" : "allocated", + "links" : [ + { + "ref" : "self", + "href" : "https://openstack.example.com/v1/storages/bbfddf09-4d7e-40d5-88a9-8acfb2f88c21" + }, + { + "ref" : "bookmark", + "href" : "https://openstack.example.com/storages/bbfddf09-4d7e-40d5-88a9-8acfb2f88c21" + } + ] + }, + { + "deviceId" : "4c16a45b-b029-49c4-af84-1abcf458a062", + "pooling_group_id" : "22zz3344-0099-7766-5544-33225512", + "allocate_status" : "available", + "links" : [ + { + "ref" : "self", + "href" : "https://openstack.example.com/v1/storages/4c16a45b-b029-49c4-af84-1abcf458a062" + }, + { + "ref" : "bookmark", + "href" : "https://openstack.example.com/storages/4c16a45b-b029-49c4-af84-1abcf458a062" + } + ] + } + ] +} diff --git a/doc/api/VALENCE_API-v0.4.1.docx b/doc/api/VALENCE_API-v0.4.1.docx new file mode 100644 index 0000000000000000000000000000000000000000..bc33aead1fbf295b2db7142dc1dd75ca82eadbcd GIT binary patch literal 73113 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 literal 0 HcmV?d00001 diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..efeb3b4 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'plasma' +copyright = u'2016, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..ed77c12 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..ea0c79d --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. plasma documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to plasma's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/init/valence-api.conf b/doc/source/init/valence-api.conf new file mode 100644 index 0000000..b1dc8bf --- /dev/null +++ b/doc/source/init/valence-api.conf @@ -0,0 +1,15 @@ +description "Valence API server" + +start on runlevel [2345] +stop on runlevel [!2345] + +env PYTHON_HOME=/home/${CHUID}/.local/bin + +# change the chuid to match yours +exec start-stop-daemon --start --verbose --chuid ${CHUID} \ +--name valence-api \ +--exec $PYTHON_HOME/valence-api -- \ +--log-file=/var/log/valence/valence-api.log + +respawn + diff --git a/doc/source/init/valence-controller.conf b/doc/source/init/valence-controller.conf new file mode 100755 index 0000000..8f33832 --- /dev/null +++ b/doc/source/init/valence-controller.conf @@ -0,0 +1,14 @@ +description "Valence Controller server" + +start on runlevel [2345] +stop on runlevel [!2345] + +env PYTHON_HOME=/home/${CHUID}/.local/bin + +exec start-stop-daemon --start --verbose --chuid ${CHUID} \ +--name valence-controller \ +--exec $PYTHON_HOME/valence-controller -- \ +--log-file=/var/log/valence/valence-controller.log + +respawn + diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..183f65b --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install plasma + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv plasma + $ pip install plasma \ No newline at end of file diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..38ba804 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..1cb0b41 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use plasma in a project:: + + import plasma \ No newline at end of file diff --git a/doc/ui-proxy/apache/README.md b/doc/ui-proxy/apache/README.md new file mode 100644 index 0000000..61b1a71 --- /dev/null +++ b/doc/ui-proxy/apache/README.md @@ -0,0 +1,71 @@ +Apache proxy service to pod-manager API +======================================= + +This manual has been verified on Ubuntu 16.04 + Apache (2.4.18-2ubuntu3.1). + +##Install +1. Use package manager tool on your distribution to install apache server. + ``` + sudo apt-get install apache2 + ``` +2. Enable all related modules for Apache server. + ``` + sudo a2enmod proxy_http proxy ssl headers + ``` +3. Setup virtual host for proxy to podm. + ``` + sudo cp podm-proxy.conf /etc/apache2/sites-available + sudo a2ensite podm-proxy + ``` +4. Add listening port 6000. + Add "Listen 6000" into Apaches port setting file /etc/apache2/ports.conf. + * If need, you can change it to any available port in your server. In this case, please remember to update + "" in /etc/apache2/sites-available/podm-proxy.conf. +5. Update podm address in /etc/apache2/sites-available/podm-proxy.conf. + By default, the podm api is pointed to https://127.0.0.1:8443/. Update it to fit your environment. +6. Restart Apache server. + ``` + sudo systemctl restart apache2 + ``` + +The proxy is available under http://127.0.0.1:6000/redfish/v1. + ``` + curl http://127.0.0.1:6000/redfish/v1/ + { + "@odata.context" : "/redfish/v1/$metadata#ServiceRoot", + "@odata.id" : "/redfish/v1", + "@odata.type" : "#ServiceRoot.1.0.0.ServiceRoot", + "Id" : "ServiceRoot", + "Name" : "Service root", + "RedfishVersion" : "1.0.0", + "UUID" : "3c414ee3-bd28-4e6c-b9e8-fd8008dbd0ce", + "Chassis" : { + "@odata.id" : "/redfish/v1/Chassis" + }, + "Services" : { + "@odata.id" : "/redfish/v1/Services" + }, + "Systems" : { + "@odata.id" : "/redfish/v1/Systems" + }, + "Managers" : { + "@odata.id" : "/redfish/v1/Managers" + }, + "EventService" : { + "@odata.id" : "/redfish/v1/EventService" + }, + "Nodes" : { + "@odata.id" : "/redfish/v1/Nodes" + }, + "EthernetSwitches" : { + "@odata.id" : "/redfish/v1/EthernetSwitches" + }, + "Oem" : { + "Intel_RackScale" : { + "@odata.type" : "#Intel.Oem.ServiceRoot", + "ApiVersion" : "1.2.0" + } + }, + "Links" : { } + } + ``` diff --git a/doc/ui-proxy/apache/podm-proxy.conf b/doc/ui-proxy/apache/podm-proxy.conf new file mode 100644 index 0000000..341fce0 --- /dev/null +++ b/doc/ui-proxy/apache/podm-proxy.conf @@ -0,0 +1,49 @@ + + # Reserve proxy to podm + ProxyRequests Off + + # If needed, change following default pod address https://127.0.0.1:8443/ + # to real podm api in your environment. + ProxyPass / https://127.0.0.1:8443/ + ProxyPassReverse / https://127.0.0.1:8443/ + + + Order Deny,Allow + Allow from all + + + # Ignore ssl certificate check when proxy request to podm + SSLProxyEngine on + SSLProxyVerify none + SSLProxyCheckPeerCN off + SSLProxyCheckPeerName off + SSLProxyCheckPeerExpire off + + # Append http header in request to podm to set up authorization. + # Default username/password: admin/admin. Please change to fit your specific setting. + RequestHeader set Authorization 'Basic YWRtaW46YWRtaW4=' + RequestHeader set User-Agent 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)' + + # Append http header in response to enable CORS + Header set Access-Control-Allow-Origin "*" + Header set Access-Control-Allow-Methods "GET, POST, PUT, OPTIONS" + Header set Access-Control-Allow-Headers "Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token" + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/etc/valence/valence.conf.sample b/etc/valence/valence.conf.sample new file mode 100644 index 0000000..de551c0 --- /dev/null +++ b/etc/valence/valence.conf.sample @@ -0,0 +1,40 @@ +[DEFAULT] +# Show more verbose log output (sets INFO log level output) +verbose = True + +# Show debugging output in logs (sets DEBUG log level output) +debug = False + +auth_strategy=noauth + +# Log to this file. Make sure the user running rsc has +# permissions to write to this file! +log_file = rsc.log + + +log_dir=/var/log/rsc +rpc_response_timeout = 300 + + +[api] +#address to bind the server to +bind_host = 0.0.0.0 + +# Port the bind the server to +bind_port = 8181 + +[oslo_messaging_rabbit] +rabbit_host = localhost +rabbit_port = 5672 +rabbit_userid = rsc +rabbit_password = rsc + +[podm] +#url=http://10.223.197.204 +url=http:// +user= +password= + +[conductor] +#topic=rsc-conductor + diff --git a/install_valence.sh b/install_valence.sh new file mode 100755 index 0000000..5d4e5fa --- /dev/null +++ b/install_valence.sh @@ -0,0 +1,42 @@ +#!/bin/bash - +#title :install_valence.sh +#description :This script will install valence package and deploys conf files +#author :Intel Corporation +#date :21-09-2016 +#version :0.1 +#usage :bash mkscript.sh +#notes :Run this script as sudo user and not as root. +# This script is needed still valence is packaged in to .deb/.rpm +#============================================================================== + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +echo $USER + +cd $DIR + +echo "Executing the script inside " +pwd + + + +# Copy the config files +sed s/\${CHUID}/$USER/ $DIR/doc/source/init/valence-api.conf > /tmp/valence-api.conf +sudo mv /tmp/valence-api.conf /etc/init/valence-api.conf +sed s/\${CHUID}/$USER/ $DIR/doc/source/init/valence-controller.conf > /tmp/valence-controller.conf +sudo mv /tmp/valence-controller.conf /etc/init/valence-controller.conf + +# create conf directory for valence +sudo mkdir /etc/valence +sudo chown ${USER}:${USER} /etc/valence +sudo cp etc/valence/valence.conf.sample /etc/valence/valence.conf + + +# create log directory for valence +sudo mkdir /var/log/valence +sudo chown ${USER}:${USER} /var/log/valence + +python setup.py install --user + +echo "Installation Completed" +echo "To start api : service valence-api start" +echo "To start controller : service valence-controller start" diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..efa2b46 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Plasma Release Notes documentation build configuration file, created by +# sphinx-quickstart on Tue Nov 3 17:40:50 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'plasma Release Notes' +copyright = u'2016, Plasma Developers' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PlasmaReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'PlasmaReleaseNotes.tex', u'Plasma Release Notes Documentation', + u'Plasma Developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'plasmareleasenotes', u'Plasma Release Notes Documentation', + [u'Plasma Developers'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'PlasmaReleaseNotes', u'Plasma Release Notes Documentation', + u'Plasma Developers', 'PlasmaReleaseNotes', + 'Openstack Plasma Project', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..8856396 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,8 @@ +============================================ + plasma Release Notes +============================================ + +.. toctree:: + :maxdepth: 1 + + unreleased diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..cd22aab --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================== + Current Series Release Notes +============================== + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dd3ff3c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,41 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +pbr>=1.6 +Babel>=2.3.4 +Paste>=2.0.3 +PasteDeploy>=1.5.2 +PyYAML>=3.11 +WebOb>=1.6.1 +amqp<=2.0 +anyjson>=0.3.3 +argparse>=1.2.1 +contextlib2>=0.5.3 +eventlet>=0.19.0 +greenlet>=0.4.10 +kombu>=3.0.35 +logutils>=0.3.3 +monotonic>=1.1 +netaddr>=0.7.18 +netifaces>=0.10.4 +oslo.concurrency>=3.10.0 +oslo.config>=3.11.0 +oslo.context>=2.5.0 +oslo.i18n>=3.7.0 +oslo.log>=3.10.0 +oslo.messaging>=5.4.0 +oslo.middleware>=3.13.0 +oslo.reports>=1.11.0 +oslo.serialization>=2.9.0 +oslo.service>=1.12.0 +oslo.utils>=3.13.0 +oslo.versionedobjects>=1.12.0 +pecan>=1.1.1 +requests>=2.10.0 +six>=1.10.0 +stevedore>=1.15.0 +waitress>=0.9.0 +wrapt>=1.10.8 +wsgiref>=0.1.2 + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4899b17 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,59 @@ +[metadata] +name = valence +summary = Openstack Valence Project +description-file = + README.rst +author = Intel Corporation +author-email = openstack-dev@lists.openstack.org +home-page = https://launchpad.net/plasma +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + valence + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = valence/locale +domain = valence + +[update_catalog] +domain = valence +output_dir = valence/locale +input_file = valence/locale/valence.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = valence/locale/valence.pot + +[build_releasenotes] +all_files = 1 +build-dir = releasenotes/build +source-dir = releasenotes/source + +[entry_points] +console_scripts = + valence-api = valence.cmd.api:main + valence-controller = valence.cmd.controller:main + +oslo.config.opts = + valence = valence.api.config:list_opts diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..056c16c --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..d354162 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,17 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking<0.11,>=0.10.0 + +coverage>=3.6 +python-subunit>=0.0.18 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 +oslosphinx>=2.5.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +testrepository>=0.0.18 +testscenarios>=0.4 +testtools>=1.4.0 + +# releasenotes +reno>=1.6.2 # Apache2 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8ac56c5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,64 @@ +[tox] +minversion = 2.0 +envlist = py34-constraints,py27-constraints,pep8-constraints +skipsdist = True + +[testenv] +usedevelop = True +install_command = + constraints: {[testenv:common-constraints]install_command} + pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/test-requirements.txt +commands = python setup.py test --slowest --testr-args='{posargs}' + +[testenv:common-constraints] +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:pep8-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:venv-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:cover-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:releasenotes] +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + +[testenv:docs-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = python setup.py build_sphinx + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[testenv:debug-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build diff --git a/valence/__init__.py b/valence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/api/__init__.py b/valence/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/api/app.py b/valence/api/app.py new file mode 100644 index 0000000..fa8acd8 --- /dev/null +++ b/valence/api/app.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from oslo_config import cfg +from oslo_middleware import request_id +from oslo_service import service +from pecan import configuration +from pecan import make_app +from valence.api import hooks +from valence.common import exceptions as p_excp + +def setup_app(*args, **kwargs): + config = { + 'server': { + 'host': cfg.CONF.api.bind_port, + 'port': cfg.CONF.api.bind_host + }, + 'app': { + 'root': 'valence.api.controllers.root.RootController', + 'modules': ['valence.api'], + 'errors': { + 400: '/error', + '__force_dict__': True + } + } + } + pecan_config = configuration.conf_from_dict(config) + + app_hooks = [hooks.CORSHook()] + + app = make_app( + pecan_config.app.root, + hooks=app_hooks, + force_canonical = False, + logging=getattr(config, 'logging', {}) + ) + return app + + +_launcher = None + + +def serve(api_service, conf, workers=1): + global _launcher + if _launcher: + raise RuntimeError('serve() can only be called once') + + _launcher = service.launch(conf, api_service, workers=workers) + + +def wait(): + _launcher.wait() diff --git a/valence/api/config.py b/valence/api/config.py new file mode 100644 index 0000000..d168cc9 --- /dev/null +++ b/valence/api/config.py @@ -0,0 +1,66 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from oslo_config import cfg +from oslo_log import log as logging +from valence.common import rpc +import sys + +LOG = logging.getLogger(__name__) + +common_opts = [ + cfg.StrOpt('auth_strategy', default='noauth', + help=("The type of authentication to use")), + cfg.BoolOpt('allow_pagination', default=False, + help=("Allow the usage of the pagination")), + cfg.BoolOpt('allow_sorting', default=False, + help=("Allow the usage of the sorting")), + cfg.StrOpt('pagination_max_limit', default="-1", + help=("The maximum number of items returned in a single " + "response, value was 'infinite' or negative integer " + "means no limit")), +] + +api_opts = [ + cfg.StrOpt('bind_host', default='0.0.0.0', + help=("The host IP to bind to")), + cfg.IntOpt('bind_port', default=8181, + help=("The port to bind to")), + cfg.IntOpt('api_workers', default=2, + help=("number of api workers")) +] + + +def init(args, **kwargs): + # Register the configuration options + api_conf_group = cfg.OptGroup(name='api', title='Valence API options') + cfg.CONF.register_group(api_conf_group) + cfg.CONF.register_opts(api_opts, group=api_conf_group) + cfg.CONF.register_opts(common_opts) + logging.register_options(cfg.CONF) + + cfg.CONF(args=args, project='valence', + **kwargs) + + rpc.init(cfg.CONF) + + +def setup_logging(): + """Sets up the logging options for a log with supplied name.""" + product_name = "valence" + logging.setup(cfg.CONF, product_name) + LOG.info("Logging enabled!") + LOG.debug("command line: %s", " ".join(sys.argv)) + + +def list_opts(): + yield None, common_opts diff --git a/valence/api/controllers/__init__.py b/valence/api/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/api/controllers/base.py b/valence/api/controllers/base.py new file mode 100644 index 0000000..86f40c2 --- /dev/null +++ b/valence/api/controllers/base.py @@ -0,0 +1,35 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class APIBase(object): + + def __init__(self, **kwargs): + for field in self.fields: + if field in kwargs: + value = kwargs[field] + setattr(self, field, value) + + def __setattr__(self, field, value): + if field in self.fields: + validator = self.fields[field]['validate'] + value = validator(value) + super(APIBase, self).__setattr__(field, value) + + def as_dict(self): + """Render this object as a dict of its fields.""" + return {f: getattr(self, f) + for f in self.fields + if hasattr(self, f)} + + def __json__(self): + return self.as_dict() diff --git a/valence/api/controllers/link.py b/valence/api/controllers/link.py new file mode 100644 index 0000000..d3bc271 --- /dev/null +++ b/valence/api/controllers/link.py @@ -0,0 +1,56 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import pecan +from valence.api.controllers import base +from valence.api.controllers import types + + +def build_url(resource, resource_args, bookmark=False, base_url=None): + if base_url is None: + base_url = pecan.request.host_url + + template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s' + # FIXME(lucasagomes): I'm getting a 404 when doing a GET on + # a nested resource that the URL ends with a '/'. + # https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs + template += '%(args)s' if resource_args.startswith('?') else '/%(args)s' + return template % {'url': base_url, 'res': resource, 'args': resource_args} + + +class Link(base.APIBase): + """A link representation.""" + + fields = { + 'href': { + 'validate': types.Text.validate + }, + 'rel': { + 'validate': types.Text.validate + }, + 'type': { + 'validate': types.Text.validate + }, + } + + @staticmethod + def make_link(rel_name, url, resource, resource_args, + bookmark=False, type=None): + href = build_url(resource, resource_args, + bookmark=bookmark, base_url=url) + if type is None: + return Link(href=href, rel=rel_name) + else: + return Link(href=href, rel=rel_name, type=type) diff --git a/valence/api/controllers/root.py b/valence/api/controllers/root.py new file mode 100644 index 0000000..b5136c8 --- /dev/null +++ b/valence/api/controllers/root.py @@ -0,0 +1,78 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from pecan import expose +from pecan import request +from pecan import route +from valence.api.controllers import base +from valence.api.controllers import link +from valence.api.controllers import types +from valence.api.controllers.v1 import controller as v1controller + + +class Version(base.APIBase): + """An API version representation.""" + + fields = { + 'id': { + 'validate': types.Text.validate + }, + 'links': { + 'validate': types.List(types.Custom(link.Link)).validate + }, + } + + @staticmethod + def convert(id): + version = Version() + version.id = id + version.links = [link.Link.make_link('self', request.host_url, + id, '', bookmark=True)] + return version + + +class Root(base.APIBase): + + fields = { + 'id': { + 'validate': types.Text.validate + }, + 'description': { + 'validate': types.Text.validate + }, + 'versions': { + 'validate': types.List(types.Custom(Version)).validate + }, + 'default_version': { + 'validate': types.Custom(Version).validate + }, + } + + @staticmethod + def convert(): + root = Root() + root.name = "OpenStack Valence API" + root.description = ("Valence is an OpenStack project") + root.versions = [Version.convert('v1')] + root.default_version = Version.convert('v1') + return root + + +class RootController(object): + @expose('json') + def index(self): + return Root.convert() + +route(RootController, 'v1', v1controller.V1Controller()) diff --git a/valence/api/controllers/types.py b/valence/api/controllers/types.py new file mode 100644 index 0000000..7beeea3 --- /dev/null +++ b/valence/api/controllers/types.py @@ -0,0 +1,132 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import six +from oslo_utils import strutils +from valence.common import exceptions as exception + +LOG = logging.getLogger(__name__) + + +class Text(object): + type_name = 'Text' + + @classmethod + def validate(cls, value): + if value is None: + return None + + if not isinstance(value, six.string_types): + raise exception.InvalidValue(value=value, type=cls.type_name) + + return value + + +class String(object): + type_name = 'String' + + @classmethod + def validate(cls, value, min_length=0, max_length=None): + if value is None: + return None + + try: + strutils.check_string_length(value, min_length=min_length, + max_length=max_length) + except TypeError: + raise exception.InvalidValue(value=value, type=cls.type_name) + except ValueError as e: + raise exception.InvalidValue(message=str(e)) + + return value + + +class Integer(object): + type_name = 'Integer' + + @classmethod + def validate(cls, value, minimum=None): + if value is None: + return None + + if not isinstance(value, six.integer_types): + try: + value = int(value) + except Exception: + LOG.exception('Failed to convert value to int') + raise exception.InvalidValue(value=value, type=cls.type_name) + + if minimum is not None and value < minimum: + message = _("Integer '%(value)s' is smaller than " + "'%(min)d'.") % {'value': value, 'min': minimum} + raise exception.InvalidValue(message=message) + + return value + + +class Bool(object): + type_name = 'Bool' + + @classmethod + def validate(cls, value, default=None): + if value is None: + value = default + + if not isinstance(value, bool): + try: + value = strutils.bool_from_string(value, strict=True) + except Exception: + LOG.exception('Failed to convert value to bool') + raise exception.InvalidValue(value=value, type=cls.type_name) + + return value + + +class Custom(object): + def __init__(self, user_class): + super(Custom, self).__init__() + self.user_class = user_class + self.type_name = self.user_class.__name__ + + def validate(self, value): + if value is None: + return None + + if not isinstance(value, self.user_class): + try: + value = self.user_class(**value) + except Exception: + LOG.exception('Failed to validate received value') + raise exception.InvalidValue(value=value, type=self.type_name) + + return value + + +class List(object): + def __init__(self, type): + super(List, self).__init__() + self.type = type + self.type_name = 'List(%s)' % self.type.type_name + + def validate(self, value): + if value is None: + return None + + if not isinstance(value, list): + raise exception.InvalidValue(value=value, type=self.type_name) + + try: + return [self.type.validate(v) for v in value] + except Exception: + LOG.exception('Failed to validate received value') + raise exception.InvalidValue(value=value, type=self.type_name) diff --git a/valence/api/controllers/v1/__init__.py b/valence/api/controllers/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/api/controllers/v1/controller.py b/valence/api/controllers/v1/controller.py new file mode 100644 index 0000000..a275b63 --- /dev/null +++ b/valence/api/controllers/v1/controller.py @@ -0,0 +1,84 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from pecan import expose +from pecan import request +from pecan import route +from valence.api.controllers import base +from valence.api.controllers import link +from valence.api.controllers import types +from valence.api.controllers.v1 import flavor as v1flavor +from valence.api.controllers.v1 import nodes as v1nodes + + +class MediaType(base.APIBase): + """A media type representation.""" + + fields = { + 'base': { + 'validate': types.Text.validate + }, + 'type': { + 'validate': types.Text.validate + }, + } + + +class V1(base.APIBase): + """The representation of the version 1 of the API.""" + + fields = { + 'id': { + 'validate': types.Text.validate + }, + 'media_types': { + 'validate': types.List(types.Custom(MediaType)).validate + }, + 'links': { + 'validate': types.List(types.Custom(link.Link)).validate + }, + 'services': { + 'validate': types.List(types.Custom(link.Link)).validate + }, + } + + @staticmethod + def convert(): + v1 = V1() + v1.id = "v1" + v1.links = [link.Link.make_link('self', request.host_url, + 'v1', '', bookmark=True), + link.Link.make_link('describedby', + 'http://docs.openstack.org', + 'developer/valence/dev', + 'api-spec-v1.html', + bookmark=True, type='text/html')] + v1.media_types = [MediaType(base='application/json', + type='application/vnd.openstack.valence.v1+json')] + v1.services = [link.Link.make_link('self', request.host_url, + 'services', ''), + link.Link.make_link('bookmark', + request.host_url, + 'services', '', + bookmark=True)] + return v1 + + +class V1Controller(object): + @expose('json') + def index(self): + return V1.convert() + +route(V1Controller, 'flavor', v1flavor.FlavorController()) +route(V1Controller, 'nodes', v1nodes.NodesController()) diff --git a/valence/api/controllers/v1/flavor.py b/valence/api/controllers/v1/flavor.py new file mode 100644 index 0000000..f4b94f3 --- /dev/null +++ b/valence/api/controllers/v1/flavor.py @@ -0,0 +1,44 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log as logging +from pecan import expose +from pecan import request +from valence.controller import api as controller_api + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class FlavorController(object): + + def __init__(self, *args, **kwargs): + super(FlavorController, self).__init__(*args, **kwargs) + + # HTTP GET /flavor/ + @expose(generic=True, template='json') + def index(self): + LOG.debug("GET /flavor") + rpcapi = controller_api.API(context=request.context) + res = rpcapi.flavor_options() + return res + + # HTTP POST /flavor/ + @index.when(method='POST', template='json') + def index_POST(self, **kw): + LOG.debug("POST /flavor") + rpcapi = controller_api.API(context=request.context) + res = rpcapi.flavor_generate(criteria=kw['criteria']) + return res diff --git a/valence/api/controllers/v1/nodes.py b/valence/api/controllers/v1/nodes.py new file mode 100644 index 0000000..8808e4e --- /dev/null +++ b/valence/api/controllers/v1/nodes.py @@ -0,0 +1,84 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log as logging +import pecan +from pecan import expose +from pecan import request +from pecan import response +from pecan.rest import RestController +from valence.controller import api as controller_api + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +#class NodeDetailController(object): +class NodeDetailController(RestController): + def __init__(self, nodeid): + self.nodeid = nodeid + + # HTTP GET /nodes/ + @expose() + def delete(self): + LOG.debug("DELETE /nodes") + rpcapi = controller_api.API(context=request.context) + res = rpcapi.delete_composednode(nodeid=self.nodeid) + LOG.info(str(res)) + return res + + @expose() + def storages(self): + pecan.abort(501, "/nodes/node id/storages") + + +class NodesController(RestController): + + def __init__(self, *args, **kwargs): + super(NodesController, self).__init__(*args, **kwargs) + + # HTTP GET /nodes/ + @expose(template='json') + def get_all(self, **kwargs): + LOG.debug("GET /nodes") + rpcapi = controller_api.API(context=request.context) + res = rpcapi.list_nodes(filters=kwargs) + return res + + # HTTP GET /nodes/ +# @index.when(method='POST', template='json') + @expose(template='json') + def post(self, **kwargs): + LOG.debug("POST /nodes") + rpcapi = controller_api.API(context=request.context) + res = rpcapi.compose_nodes(criteria=kwargs) + return res + + @expose(template='json') + def get(self, nodeid): + LOG.debug("GET /nodes" + nodeid) + rpcapi = controller_api.API(context=request.context) + node = rpcapi.get_nodebyid(nodeid=nodeid) + if not node: + pecan.abort(404) + return node + + @expose() + def _lookup(self, nodeid, *remainder): + # node = get_student_by_primary_key(primary_key) + if nodeid: + return NodeDetailController(nodeid), remainder + else: + pecan.abort(404) diff --git a/valence/api/controllers/v1/storages.py b/valence/api/controllers/v1/storages.py new file mode 100644 index 0000000..f75d393 --- /dev/null +++ b/valence/api/controllers/v1/storages.py @@ -0,0 +1,44 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log as logging +import pecan +from pecan import expose +from pecan import request +from valence.controller import api as controller_api + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class StoragesController(object): + + def __init__(self, *args, **kwargs): + super(StoragesController, self).__init__(*args, **kwargs) + + # HTTP GET /storages/ + @expose(generic=True, template='json') + def index(self): + LOG.debug("GET /storages") + rpcapi = controller_api.API(context=request.context) + LOG.debug(rpcapi) + pecan.abort(501, "GET /storages is Not yet implemented") + + @expose(template='json') + def get(self, storageid): + LOG.debug("GET /storages" + storageid) + rpcapi = controller_api.API(context=request.context) + LOG.debug(rpcapi) + pecan.abort(501, "GET /storages/storage is Not yet implemented") diff --git a/valence/api/hooks.py b/valence/api/hooks.py new file mode 100644 index 0000000..aa141c2 --- /dev/null +++ b/valence/api/hooks.py @@ -0,0 +1,13 @@ +from oslo_config import cfg +from pecan.hooks import PecanHook + + +class CORSHook(PecanHook): + + def after(self, state): + state.response.headers['Access-Control-Allow-Origin'] = '*' + state.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, PUT, LIST, OPTIONS' + state.response.headers['Access-Control-Allow-Headers'] = 'origin, authorization, content-type, accept' + if not state.response.headers['Content-Length']: + state.response.headers['Content-Length'] = str(len(state.response.body)) + diff --git a/valence/cmd/__init__.py b/valence/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/cmd/api.py b/valence/cmd/api.py new file mode 100755 index 0000000..7841006 --- /dev/null +++ b/valence/cmd/api.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# copyright (c) 2016 Intel, Inc. +# +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import wsgi + +from valence.api import app +from valence.api import config as api_config + +CONF = cfg.CONF +LOG = logging.getLogger('valence.api') + + +def main(): + api_config.init(sys.argv[1:]) + api_config.setup_logging() + application = app.setup_app() + host = CONF.api.bind_host + port = CONF.api.bind_port + workers = 1 + + LOG.info(("Server on http://%(host)s:%(port)s with %(workers)s"), + {'host': host, 'port': port, 'workers': workers}) + + service = wsgi.Server(CONF, "valence", application, host, port) + + app.serve(service, CONF, workers) + + LOG.info("Configuration:") + app.wait() + + +if __name__ == '__main__': + main() diff --git a/valence/cmd/controller.py b/valence/cmd/controller.py new file mode 100644 index 0000000..be2a3eb --- /dev/null +++ b/valence/cmd/controller.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Starter script for the Valence controller service.""" + +import os +from oslo_config import cfg +from oslo_log import log as logging +from oslo_service import service +from valence.common import rpc_service +from valence.controller import config as controller_config +from valence.controller.handlers import flavor_controller +from valence.controller.handlers import node_controller +# from valence import version +import sys +import uuid + +LOG = logging.getLogger(__name__) + + +def main(): + controller_config.init(sys.argv[1:]) + controller_config.setup_logging() + LOG.info(('Starting valence-controller in PID %s'), os.getpid()) + LOG.debug("Configuration:") +# cfg.CONF.import_opt('topic', +# 'valence.controller.config', +# group='controller') + + controller_id = uuid.uuid4() + endpoints = [ + flavor_controller.Handler(), + node_controller.Handler() + ] + + server = rpc_service.Service.create(cfg.CONF.controller.topic, + controller_id, endpoints, + binary='valence-controller') + launcher = service.launch(cfg.CONF, server) + launcher.wait() + +if __name__ == '__main__': + main() diff --git a/valence/common/__init__.py b/valence/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/common/context.py b/valence/common/context.py new file mode 100644 index 0000000..c0d7641 --- /dev/null +++ b/valence/common/context.py @@ -0,0 +1,75 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from oslo_context import context as oslo_ctx + + +class ContextBase(oslo_ctx.RequestContext): + def __init__(self, auth_token=None, user_id=None, tenant_id=None, + is_admin=False, request_id=None, overwrite=True, + user_name=None, tenant_name=None, auth_url=None, + region=None, password=None, domain='default', + project_name=None, **kwargs): + super(ContextBase, self).__init__( + auth_token=auth_token, + user=user_id or kwargs.get('user', None), + tenant=tenant_id or kwargs.get('tenant', None), + domain=kwargs.get('domain', None), + user_domain=kwargs.get('user_domain', None), + project_domain=kwargs.get('project_domain', None), + is_admin=is_admin, + read_only=kwargs.get('read_only', False), + show_deleted=kwargs.get('show_deleted', False), + request_id=request_id, + resource_uuid=kwargs.get('resource_uuid', None), + overwrite=overwrite) + self.user_name = user_name + self.tenant_name = tenant_name + self.tenant_id = tenant_id + self.auth_url = auth_url + self.password = password + self.default_name = domain + self.region_name = region + self.project_name = project_name + + def to_dict(self): + ctx_dict = super(ContextBase, self).to_dict() + # ctx_dict.update({ + # to do : dict update + # }) + return ctx_dict + + @classmethod + def from_dict(cls, ctx): + return cls(**ctx) + + +class Context(ContextBase): + def __init__(self, **kwargs): + super(Context, self).__init__(**kwargs) + self._session = None + + @property + def session(self): + return self._session + + +def get_admin_context(read_only=True): + return ContextBase(user_id=None, + project_id=None, + is_admin=True, + overwrite=False, + read_only=read_only) + + +def get_current(): + return oslo_ctx.get_current() diff --git a/valence/common/exceptions.py b/valence/common/exceptions.py new file mode 100644 index 0000000..f195416 --- /dev/null +++ b/valence/common/exceptions.py @@ -0,0 +1,79 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +RSC base exception handling. +""" +import six + +from oslo_utils import excutils + + +class RSCException(Exception): + """Base RSC Exception.""" + + message = "An unknown exception occurred." + + def __init__(self, **kwargs): + try: + super(RSCException, self).__init__(self.message % kwargs) + self.msg = self.message % kwargs + except Exception: + with excutils.save_and_reraise_exception() as ctxt: + if not self.use_fatal_exceptions(): + ctxt.reraise = False + # at least get the core message out if something happened + super(RSCException, self).__init__(self.message) + + if six.PY2: + def __unicode__(self): + return unicode(self.msg) + + def use_fatal_exceptions(self): + return False + + +class BadRequest(RSCException): + message = 'Bad %(resource)s request' + + +class NotImplemented(RSCException): + message = ("Not yet implemented in RSC %(func_name)s: ") + + +class NotFound(RSCException): + message = ("URL not Found") + + +class Conflict(RSCException): + pass + + +class ServiceUnavailable(RSCException): + message = "The service is unavailable" + + +class ConnectionRefused(RSCException): + message = "Connection to the service endpoint is refused" + + +class TimeOut(RSCException): + message = "Timeout when connecting to OpenStack Service" + + +class InternalError(RSCException): + message = "Error when performing operation" + + +class InvalidInputError(RSCException): + message = ("An invalid value was provided for %(opt_name)s: " + "%(opt_value)s") diff --git a/valence/common/osinterface.py b/valence/common/osinterface.py new file mode 100644 index 0000000..228c635 --- /dev/null +++ b/valence/common/osinterface.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import json +from oslo_config import cfg +from oslo_log import log as logging +import requests +from requests.auth import HTTPBasicAuth + + +LOG = logging.getLogger(__name__) +cfg.CONF.import_group('undercloud', 'valence.controller.config') + + +def _send_request(url, method, headers, requestbody=None): + defaultheaders = {'Content-Type': 'application/json'} + auth = HTTPBasicAuth(cfg.CONF.undercloud.os_user, + cfg.CONF.undercloud.os_password) + headers = defaultheaders.update(headers) + LOG.debug(url) + resp = requests.request(method, + url, + headers=defaultheaders, + data=requestbody, + auth=auth) + LOG.debug(resp.status_code) + return resp.json() + + +def _get_servicecatalogue_endpoint(keystonejson, servicename): + """Fetch particular endpoint from Keystone. + + This function is to get the particular endpoint from the + list of endpoints returned fro keystone. + + """ + + for d in keystonejson["access"]["serviceCatalog"]: + if(d["name"] == servicename): + return d["endpoints"][0]["publicURL"] + + +def _get_token_and_url(nameofendpoint): + """Fetch token from the endpoint + + This function get new token and associated endpoint. + name of endpoint carries the name of the service whose + endpoint need to be found. + + """ + + url = cfg.CONF.undercloud.os_admin_url + "/tokens" + data = {"auth": + {"tenantName": cfg.CONF.undercloud.os_tenant, + "passwordCredentials": + {"username": cfg.CONF.undercloud.os_user, + "password": cfg.CONF.undercloud.os_password}}} + rdata = _send_request(url, "POST", {}, json.dumps(data)) + tokenid = rdata["access"]["token"]["id"] + endpoint = _get_servicecatalogue_endpoint(rdata, nameofendpoint) + LOG.debug("Token,Endpoint %s: %s from keystone for %s" + % (tokenid, endpoint, nameofendpoint)) + return (tokenid, endpoint) + + +# put this function in utils.py later +def _get_imageid(jsondata, imgname): + # write a generic funciton for this and _get_servicecatalogue_endpoint + for d in jsondata["images"]: + if(d["name"] == imgname): + return d["id"] + + +def get_undercloud_images(): + tokenid, endpoint = _get_token_and_url("glance") + resp = _send_request(endpoint + "/v2/images", + "GET", + {'X-Auth-Token': tokenid}) + imagemap = {"deploy_ramdisk": _get_imageid(resp, "bm-deploy-ramdisk"), + "deploy_kernel": _get_imageid(resp, "bm-deploy-kernel"), + "image_source": _get_imageid(resp, "overcloud-full"), + "ramdisk": _get_imageid(resp, "overcloud-full-initrd"), + "kernel": _get_imageid(resp, "overcloud-full-vmlinuz")} + return imagemap diff --git a/valence/common/redfish/__init__.py b/valence/common/redfish/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/common/redfish/api.py b/valence/common/redfish/api.py new file mode 100644 index 0000000..42567be --- /dev/null +++ b/valence/common/redfish/api.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +from oslo_config import cfg +from oslo_log import log as logging +import requests +from requests.auth import HTTPBasicAuth +from valence.common.redfish import tree + +LOG = logging.getLogger(__name__) +cfg.CONF.import_group('podm', 'valence.common.redfish.config') + + +def get_rfs_url(serviceext): + REDFISH_BASE_EXT = "/redfish/v1/" + INDEX = '' + # '/index.json' + if REDFISH_BASE_EXT in serviceext: + return cfg.CONF.podm.url + serviceext + INDEX + else: + return cfg.CONF.podm.url + REDFISH_BASE_EXT + serviceext + INDEX + + +def send_request(resource, method="GET",**kwargs): + # The verify=false param in the request should be removed eventually + url = get_rfs_url(resource) + httpuser = cfg.CONF.podm.user + httppwd = cfg.CONF.podm.password + resp = None + try: + resp = requests.request(method, url, verify=False, auth=HTTPBasicAuth(httpuser, httppwd), **kwargs) + except requests.exceptions.RequestException as e: + LOG.error(e) + return resp + + +def filter_chassis(jsonContent, filterCondition): + returnJSONObj = {} + returnMembers = [] + parsed = json.loads(jsonContent) + members = parsed['Members'] + # count = parsed['Members@odata.count'] + for member in members: + resource = member['@odata.id'] + resp = send_request(resource) + memberJsonObj = json.loads(resp.json()) + chassisType = memberJsonObj['ChassisType'] + if chassisType == filterCondition: + returnMembers.append(member) + returnJSONObj["Members"] = returnMembers + returnJSONObj["Members@odata.count"] = len(returnMembers) + return returnJSONObj + + +def generic_filter(jsonContent, filterConditions): + # returns boolean based on filters..its generic filter + # returnMembers = [] + is_filter_passed = False + for fc in filterConditions: + if fc in jsonContent: + if jsonContent[fc].lower() == filterConditions[fc].lower(): + is_filter_passed = True + else: + is_filter_passed = False + break + elif "/" in fc: + querylst = fc.split("/") + tmp = jsonContent + for q in querylst: + tmp = tmp[q] + if tmp.lower() == filterConditions[fc].lower(): + is_filter_passed = True + else: + is_filter_passed = False + break + else: + LOG.warn(" Filter string mismatch ") + LOG.info(" JSON CONTENT " + str(is_filter_passed)) + return is_filter_passed + + +def get_details(source): + # count = source['Members@odata.count'] + returnJSONObj = [] + members = source['Members'] + for member in members: + resource = member['@odata.id'] + resp = send_request(resource) + memberJson = resp.json() + memberJsonObj = json.loads(memberJson) + returnJSONObj[resource] = memberJsonObj + return returnJSONObj + + +def systemdetails(): + returnJSONObj = [] + parsed = send_request('Systems') + members = parsed['Members'] + for member in members: + resource = member['@odata.id'] + resp = send_request(resource) + memberJsonContent = resp.json() + memberJSONObj = json.loads(memberJsonContent) + returnJSONObj[resource] = memberJSONObj + return(json.dumps(returnJSONObj)) + + +def nodedetails(): + returnJSONObj = [] + parsed = send_request('Nodes') + members = parsed['Members'] + for member in members: + resource = member['@odata.id'] + resp = send_request(resource) + memberJSONObj = resp.json() + returnJSONObj[resource] = memberJSONObj + return(json.dumps(returnJSONObj)) + + +def podsdetails(): + jsonContent = send_request('Chassis') + pods = filter_chassis(jsonContent, 'Pod') + podsDetails = get_details(pods) + return json.dumps(podsDetails) + + +def racksdetails(): + jsonContent = send_request('Chassis') + racks = filter_chassis(jsonContent, 'Rack') + racksDetails = get_details(racks) + return json.dumps(racksDetails) + + +def racks(): + jsonContent = send_request('Chassis') + racks = filter_chassis(jsonContent, 'Rack') + return json.dumps(racks) + + +def pods(): + jsonContent = send_request('Chassis') + pods = filter_chassis(jsonContent, 'Pod') + return json.dumps(pods) + + +def urls2list(url): + # This will extract the url values from @odata.id inside Members + resp = send_request(url) + respdata = resp.json() + return [u['@odata.id'] for u in respdata['Members']] + + +def extract_val(data, path): + # function to select value at particularpath + patharr = path.split("/") + for p in patharr: + data = data[p] + return data + + +def node_cpu_details(nodeurl): + cpucnt = 0 + cpuarch = "" + cpulist = urls2list(nodeurl + '/Processors') + for lnk in cpulist: + LOG.info("Processing CPU %s" % lnk) + resp = send_request(lnk) + respdata = resp.json() + cpucnt += extract_val(respdata, "TotalCores") + cpuarch = extract_val(respdata, "InstructionSet") + cpumodel = extract_val(respdata, "Model") + LOG.debug(" Cpu details %s: %d: %s: %s " + % (nodeurl, cpucnt, cpuarch, cpumodel)) + return {"count": str(cpucnt), "arch": cpuarch, "model": cpumodel} + + +def node_ram_details(nodeurl): + # this extracts the RAM and returns as dictionary + resp = send_request(nodeurl) + respjson = resp.json() + ram = extract_val(respjson, "MemorySummary/TotalSystemMemoryGiB") + #LOG.debug(" Total Ram for node %s : %d " % (nodeurl, ram)) + return str(ram) if ram else "0" + + +def node_nw_details(nodeurl): + # this extracts the total nw interfaces and returns as a string + resp = send_request(nodeurl + "/EthernetInterfaces") + respbody = resp.json() + nwi = extract_val(respbody, "Members@odata.count") + LOG.debug(" Total NW for node %s : %d " % (nodeurl, nwi)) + return str(nwi) if nwi else "0" + + +def node_storage_details(nodeurl): + # this extracts the RAM and returns as dictionary + storagecnt = 0 + hddlist = urls2list(nodeurl + "/SimpleStorage") + for lnk in hddlist: + resp = send_request(lnk) + respbody = resp.json() + hdds = extract_val(respbody, "Devices") + for sd in hdds: + if "CapacityBytes" in sd: + if sd["CapacityBytes"] is not None: + storagecnt += sd["CapacityBytes"] + LOG.debug("Total storage for node %s : %d " % (nodeurl, storagecnt)) + # to convert Bytes in to GB. Divide by 1073741824 + return str(storagecnt / 1073741824).split(".")[0] + + +def systems_list(count=None, filters={}): + # comment the count value which is set to 2 now.. + # list of nodes with hardware details needed for flavor creation + # count = 2 + lst_systems = [] + systemurllist = urls2list("Systems") + podmtree = build_hierarchy_tree() + #podmtree.writeHTML("0","/tmp/a.html") + + for lnk in systemurllist[:count]: + filterPassed = True + resp = send_request(lnk) + system = resp.json() + + # this below code need to be changed when proper query mechanism + # is implemented + if any(filters): + filterPassed = generic_filter(system, filters) + if not filterPassed: + continue + + systemid = lnk.split("/")[-1] + systemuuid = system['UUID'] + systemlocation = podmtree.getPath(lnk) + cpu = node_cpu_details(lnk) + ram = node_ram_details(lnk) + nw = node_nw_details(lnk) + storage = node_storage_details(lnk) + node = {"nodeid": systemid, "cpu": cpu, + "ram": ram, "storage": storage, + "nw": nw, "location": systemlocation, + "uuid": systemuuid} + + # filter based on RAM, CPU, NETWORK..etc + if 'ram' in filters: + filterPassed = (True + if int(ram) >= int(filters['ram']) + else False) + + # filter based on RAM, CPU, NETWORK..etc + if 'nw' in filters: + filterPassed = (True + if int(nw) >= int(filters['nw']) + else False) + + # filter based on RAM, CPU, NETWORK..etc + if 'storage' in filters: + filterPassed = (True + if int(storage) >= int(filters['storage']) + else False) + + if filterPassed: + lst_systems.append(node) + # LOG.info(str(node)) + return lst_systems + + +def get_chassis_list(): + chassis_lnk_lst = urls2list("Chassis") + lst_chassis = [] + + for clnk in chassis_lnk_lst: + resp = send_request(clnk) + data = resp.json() + LOG.info(data) + if "Links" in data: + contains = [] + containedby = {} + computersystems = [] + linksdata = data["Links"] + if "Contains" in linksdata and linksdata["Contains"]: + for c in linksdata["Contains"]: + contains.append(c['@odata.id'].split("/")[-1]) + + if "ContainedBy" in linksdata and linksdata["ContainedBy"]: + odata = linksdata["ContainedBy"]['@odata.id'] + containedby = odata.split("/")[-1] + + if "ComputerSystems" in linksdata and linksdata["ComputerSystems"]: + for c in linksdata["ComputerSystems"]: + computersystems.append(c['@odata.id']) + + name = data["ChassisType"] + ":" + data["Id"] + c = {"name": name, + "ChassisType": data["ChassisType"], + "ChassisID": data["Id"], + "Contains": contains, + "ContainedBy": containedby, + "ComputerSystems": computersystems} + lst_chassis.append(c) + return lst_chassis + + +def get_nodebyid(nodeid): + resp = send_request("Nodes/" + nodeid) + return resp.json() + + +def build_hierarchy_tree(): + # builds the tree sturcture of the PODM data to get the location hierarchy + lst_chassis = get_chassis_list() + podmtree = tree.Tree() + podmtree.add_node("0") # Add root node + for d in lst_chassis: + podmtree.add_node(d["ChassisID"], d) + + for d in lst_chassis: + containedby = d["ContainedBy"] if d["ContainedBy"] else "0" + podmtree.add_node(d["ChassisID"], d, containedby) + systems = d["ComputerSystems"] + for system in systems: + sysname = system.split("/")[-2] + ":" + system.split("/")[-1] + podmtree.add_node(system, {"name": sysname}, d["ChassisID"]) + return podmtree + +def compose_node(criteria={}): + #node comosition + composeurl = "Nodes/Actions/Allocate" + reqbody = None if not criteria else criteria + headers = {'Content-type': 'application/json'} + if not criteria: + resp = send_request(composeurl, "POST", headers = headers) + else: + resp = send_request(composeurl, "POST", json=criteria, headers = headers) + LOG.info(resp.headers) + LOG.info(resp.text) + LOG.info(resp.status_code) + composednode = resp.headers['Location'] + + return { "node" : composednode } + + +def delete_composednode(nodeid): + #delete composed node + deleteurl = "Nodes/" + str(nodeid) + resp = send_request(deleteurl, "DELETE") + return resp + +def nodes_list(count=None, filters={}): + # comment the count value which is set to 2 now.. + # list of nodes with hardware details needed for flavor creation + # count = 2 + lst_nodes = [] + nodeurllist = urls2list("Nodes") + #podmtree = build_hierarchy_tree() + #podmtree.writeHTML("0","/tmp/a.html") + + for lnk in nodeurllist: + filterPassed = True + resp = send_request(lnk) + if resp.status_code != 200: + Log.info("Error in fetching Node details " + lnk) + else: + node = resp.json() + + # this below code need to be changed when proper query mechanism + # is implemented + if any(filters): + filterPassed = generic_filter(node, filters) + if not filterPassed: + continue + + nodeid = lnk.split("/")[-1] + nodeuuid = node['UUID'] + nodelocation = node['AssetTag'] + #podmtree.getPath(lnk) commented as location should be computed using + #other logic.consult Chester + nodesystemurl = node["Links"]["ComputerSystem"]["@odata.id"] + cpu = {} + ram = 0 + nw = 0 + localstorage = node_storage_details(nodesystemurl) + if "Processors" in node: + cpu = { "count" : node["Processors"]["Count"], + "model" : node["Processors"]["Model"]} + + if "Memory" in node: + ram = node["Memory"]["TotalSystemMemoryGiB"] + + if "EthernetInterfaces" in node["Links"]: + nw = len(node["Links"]["EthernetInterfaces"]) + + storage = 0 + bmcip = "127.0.0.1" #system['Oem']['Dell_G5MC']['BmcIp'] + bmcmac = "00:00:00:00:00" #system['Oem']['Dell_G5MC']['BmcMac'] + node = {"nodeid": nodeid, "cpu": cpu, + "ram": ram, "storage": localstorage, + "nw": nw, "location": nodelocation, + "uuid": nodeuuid, "bmcip": bmcip, "bmcmac": bmcmac} + if filterPassed: + lst_nodes.append(node) + # LOG.info(str(node)) + return lst_nodes diff --git a/valence/common/redfish/config.py b/valence/common/redfish/config.py new file mode 100644 index 0000000..0b526dd --- /dev/null +++ b/valence/common/redfish/config.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + + +# Configurations +podm_opts = [ + cfg.StrOpt('url', + default='http://localhost:80', + help=("The complete url string of PODM")), + cfg.StrOpt('user', + default='admin', + help=("User for the PODM")), + cfg.StrOpt('password', + default='admin', + help=("Passoword for PODM"))] + +podm_conf_group = cfg.OptGroup(name='podm', title='RSC PODM options') +cfg.CONF.register_group(podm_conf_group) +cfg.CONF.register_opts(podm_opts, group=podm_conf_group) diff --git a/valence/common/redfish/tree.py b/valence/common/redfish/tree.py new file mode 100644 index 0000000..2e7e439 --- /dev/null +++ b/valence/common/redfish/tree.py @@ -0,0 +1,122 @@ +(_ROOT, _DEPTH, _BREADTH) = range(3) + + +class Tree(object): + + def __init__(self): + self.__nodes = {} + + @property + def nodes(self): + return self.__nodes + + def add_node(self, identifier, data={}, parent=None): + if identifier in self.nodes: + node = self[identifier] + else: + node = TreeNode(identifier,data) + self[identifier] = node + + if parent is not None: + self[parent].add_child(identifier) + self[identifier].set_parent(parent) + return node + + def display(self, identifier, depth=_ROOT): + children = self[identifier].children + # data = self[identifier].data + if depth == _ROOT: + print("{0}".format(identifier)) + else: + print("\t" * depth, "{0}".format(identifier)) + + depth += 1 + for child in children: + self.display(child, depth) # recursive call + + def processHTML(self, fileref, identifier, depth=_ROOT): + # generate the tree structure in html. + # the enclosing html should be included in the calling function + + fileref.write("
    ") + children = self[identifier].children + if self[identifier].data: + name = self[identifier].data['name'] + else: + name = identifier + + htmlstr = "
  • " + name + "[" + identifier + "]
  • " + + fileref.write(htmlstr) + depth += 1 + for child in children: + self.processHTML(fileref, child, depth) # recursive call + fileref.write("
") + + def writeHTML(self, rootnodeid, filename="chassisTree.html"): + htmlfile = open(filename, 'w+') + htmlfile.write("

Tree

") + self.processHTML(htmlfile, rootnodeid) + htmlfile.write("") + htmlfile.close() + + def traverse(self, identifier, mode=_DEPTH): + # Python generator. Loosly based on an algorithm from + # 'Essential LISP' by John R. Anderson, Albert T. Corbett, + # and Brian J. Reiser, page 239-241 + yield identifier + queue = self[identifier].children + while queue: + yield queue[0] + expansion = self[queue[0]].children + if mode == _DEPTH: + queue = expansion + queue[1:] # depth-first + elif mode == _BREADTH: + queue = queue[1:] + expansion # width-first + + def getPath(self, identifier): + if self[identifier].parent is not None: + parentpath = self.getPath(self[identifier].parent) + return self[identifier].data["name"] + "_" + parentpath + else: + if self[identifier].data: + return self[identifier].data['name'] + else: + return "" + + def __getitem__(self, key): + return self.__nodes[key] + + def __setitem__(self, key, item): + self.__nodes[key] = item + + +# Class represents Tree Node +class TreeNode(object): + def __init__(self, identifier, data={}): + self.__identifier = identifier + self.__children = [] + self.__parent = None + self.__data = data + + @property + def identifier(self): + return self.__identifier + + @property + def children(self): + return self.__children + + @property + def parent(self): + return self.__parent + + @property + def data(self): + return self.__data + + def add_child(self, identifier): + self.__children.append(identifier) + + def set_parent(self, identifier): + self.__parent = identifier diff --git a/valence/common/rpc.py b/valence/common/rpc.py new file mode 100644 index 0000000..dc2f064 --- /dev/null +++ b/valence/common/rpc.py @@ -0,0 +1,139 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# import oslo_messaging as messaging +# from oslo_serialization import jsonutils +# from valence.common import valencecontext +from oslo_config import cfg +import oslo_messaging as messaging +from oslo_serialization import jsonutils +from valence.common import context as valence_ctx +import valence.common.exceptions + + +__all__ = [ + 'init', + 'cleanup', + 'set_defaults', + 'add_extra_exmods', + 'clear_extra_exmods', + 'get_allowed_exmods', + 'RequestContextSerializer', + 'get_client', + 'get_server', + 'get_notifier', +] + +CONF = cfg.CONF +TRANSPORT = None +NOTIFIER = None + +ALLOWED_EXMODS = [ + valence.common.exceptions.__name__, +] +EXTRA_EXMODS = [] + + +def init(conf): + global TRANSPORT, NOTIFIER + exmods = get_allowed_exmods() + TRANSPORT = messaging.get_transport(conf, + allowed_remote_exmods=exmods) + serializer = RequestContextSerializer(JsonPayloadSerializer()) + NOTIFIER = messaging.Notifier(TRANSPORT, serializer=serializer) + + +def cleanup(): + global TRANSPORT, NOTIFIER + assert TRANSPORT is not None + assert NOTIFIER is not None + TRANSPORT.cleanup() + TRANSPORT = NOTIFIER = None + + +def set_defaults(control_exchange): + messaging.set_transport_defaults(control_exchange) + + +def add_extra_exmods(*args): + EXTRA_EXMODS.extend(args) + + +def clear_extra_exmods(): + del EXTRA_EXMODS[:] + + +def get_allowed_exmods(): + return ALLOWED_EXMODS + EXTRA_EXMODS + + +class JsonPayloadSerializer(messaging.NoOpSerializer): + @staticmethod + def serialize_entity(context, entity): + return jsonutils.to_primitive(entity, convert_instances=True) + + +class RequestContextSerializer(messaging.Serializer): + + def __init__(self, base): + self._base = base + + def serialize_entity(self, context, entity): + if not self._base: + return entity + return self._base.serialize_entity(context, entity) + + def deserialize_entity(self, context, entity): + if not self._base: + return entity + return self._base.deserialize_entity(context, entity) + + def serialize_context(self, context): + if isinstance(context, dict): + return context + else: + return context.to_dict() + + def deserialize_context(self, context): +# return valence.common.context.Context.from_dict(context) + return valence_ctx.Context.from_dict(context) + + +def get_transport_url(url_str=None): + return messaging.TransportURL.parse(CONF, url_str) + + +def get_client(target, version_cap=None, serializer=None): + assert TRANSPORT is not None + serializer = RequestContextSerializer(serializer) + return messaging.RPCClient(TRANSPORT, + target, + version_cap=version_cap, + serializer=serializer) + + +def get_server(target, endpoints, serializer=None): + assert TRANSPORT is not None + serializer = RequestContextSerializer(serializer) + return messaging.get_rpc_server(TRANSPORT, + target, + endpoints, + executor='eventlet', + serializer=serializer) + + +def get_notifier(service, host=None, publisher_id=None): + assert NOTIFIER is not None + if not publisher_id: + publisher_id = "%s.%s" % (service, host or CONF.host) + return NOTIFIER.prepare(publisher_id=publisher_id) diff --git a/valence/common/rpc_service.py b/valence/common/rpc_service.py new file mode 100644 index 0000000..ed7f30a --- /dev/null +++ b/valence/common/rpc_service.py @@ -0,0 +1,89 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common RPC service and API tools for Valence.""" + +import eventlet +from oslo_config import cfg +import oslo_messaging as messaging +from oslo_service import service + +from valence.common import rpc +from valence.objects import base as objects_base + +eventlet.monkey_patch() + +periodic_opts = [ + cfg.IntOpt('periodic_interval_max', + default=60, + help='Max interval size between periodic tasks execution in ' + 'seconds.'), +] + +CONF = cfg.CONF +CONF.register_opts(periodic_opts) + + +class Service(service.Service): + + def __init__(self, topic, server, handlers, binary): + super(Service, self).__init__() + serializer = rpc.RequestContextSerializer( + objects_base.ValenceObjectSerializer()) + transport = messaging.get_transport(cfg.CONF) + # TODO(asalkeld) add support for version='x.y' + target = messaging.Target(topic=topic, server=server) + self._server = messaging.get_rpc_server(transport, target, handlers, + serializer=serializer) + self.binary = binary + + def start(self): + # servicegroup.setup(CONF, self.binary, self.tg) + self._server.start() + + def stop(self): + if self._server: + self._server.stop() + self._server.wait() + super(Service, self).stop() + + @classmethod + def create(cls, topic, server, handlers, binary): + service_obj = cls(topic, server, handlers, binary) + return service_obj + + +class API(object): + def __init__(self, transport=None, context=None, topic=None, server=None, + timeout=None): + serializer = rpc.RequestContextSerializer( + objects_base.ValenceObjectSerializer()) + if transport is None: + exmods = rpc.get_allowed_exmods() + transport = messaging.get_transport(cfg.CONF, + allowed_remote_exmods=exmods) + self._context = context + if topic is None: + topic = '' + target = messaging.Target(topic=topic, server=server) + self._client = messaging.RPCClient(transport, target, + serializer=serializer, + timeout=timeout) + + def _call(self, method, *args, **kwargs): + return self._client.call(self._context, method, *args, **kwargs) + + def _cast(self, method, *args, **kwargs): + self._client.cast(self._context, method, *args, **kwargs) + + def echo(self, message): + self._cast('echo', message=message) diff --git a/valence/controller/__init__.py b/valence/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/controller/api.py b/valence/controller/api.py new file mode 100644 index 0000000..46c7f9a --- /dev/null +++ b/valence/controller/api.py @@ -0,0 +1,67 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""controller API for interfacing with Other modules""" +from oslo_config import cfg +from oslo_log import log as logging +from valence.common import rpc_service + + +# The Backend API class serves as a AMQP client for communicating +# on a topic exchange specific to the controllers. This allows the ReST +# API to trigger operations on the controllers + +LOG = logging.getLogger(__name__) + + +class API(rpc_service.API): + def __init__(self, transport=None, context=None, topic=None): + if topic is None: + cfg.CONF.import_opt('topic', 'valence.controller.config', + group='controller') + super(API, self).__init__(transport, context, + topic=cfg.CONF.controller.topic) + + # Flavor Operations + + def flavor_options(self): + return self._call('flavor_options') + + def flavor_generate(self, criteria): + return self._call('flavor_generate', criteria=criteria) + + # Node(s) Operations + def list_nodes(self, filters): + return self._call('list_nodes', filters=filters) + + def get_nodebyid(self, nodeid): + return self._call('get_nodebyid', nodeid=nodeid) + + def delete_composednode(self, nodeid): + return self._call('delete_composednode', nodeid=nodeid) + + def update_node(self, nodeid): + return self._call('update_node') + + def compose_nodes(self, criteria): + return self._call('compose_nodes', criteria=criteria) + + def list_node_storages(self, data): + return self._call('list_node_storages') + + def map_node_storage(self, data): + return self._call('map_node_storage') + + def delete_node_storage(self, data): + return self._call('delete_node_storage') diff --git a/valence/controller/config.py b/valence/controller/config.py new file mode 100644 index 0000000..8012331 --- /dev/null +++ b/valence/controller/config.py @@ -0,0 +1,65 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Config options for Valence controller Service""" + +from oslo_config import cfg +from oslo_log import log as logging +import sys + +LOG = logging.getLogger(__name__) + +CONTROLLER_OPTS = [ + cfg.StrOpt('topic', + default='valence-controller', + help='The queue to add controller tasks to.') +] + +OS_INTERFACE_OPTS = [ + cfg.StrOpt('os_admin_url', + help='Admin URL of Openstack'), + cfg.StrOpt('os_tenant', + default='admin', + help='Tenant for Openstack'), + cfg.StrOpt('os_user', + default='admin', + help='User for openstack'), + cfg.StrOpt('os_password', + default='addmin', + help='Password for openstack') +] + +controller_conf_group = cfg.OptGroup(name='controller', + title='Valence controller options') +cfg.CONF.register_group(controller_conf_group) +cfg.CONF.register_opts(CONTROLLER_OPTS, group=controller_conf_group) + +os_conf_group = cfg.OptGroup(name='undercloud', + title='Valence Openstack interface options') +cfg.CONF.register_group(os_conf_group) +cfg.CONF.register_opts(OS_INTERFACE_OPTS, group=os_conf_group) + + +def init(args, **kwargs): + # Register the configuration options + logging.register_options(cfg.CONF) + cfg.CONF(args=args, project='valence', **kwargs) + + +def setup_logging(): + """Sets up the logging options for a log with supplied name.""" + domain = "valence" + logging.setup(cfg.CONF, domain) + LOG.info("Logging enabled!") + LOG.debug("command line: %s", " ".join(sys.argv)) diff --git a/valence/controller/handlers/__init__.py b/valence/controller/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/controller/handlers/flavor_controller.py b/valence/controller/handlers/flavor_controller.py new file mode 100644 index 0000000..f4195b0 --- /dev/null +++ b/valence/controller/handlers/flavor_controller.py @@ -0,0 +1,37 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from valence.flavor import flavor + +LOG = logging.getLogger(__name__) + + +class Handler(object): + """Valence Flavor RPC handler. + + These are the backend operations. They are executed by the backend ervice. + API calls via AMQP (within the ReST API) trigger the handlers to be called. + + """ + + def __init__(self): + super(Handler, self).__init__() + + def flavor_options(self, context): + return flavor.get_available_criteria() + + def flavor_generate(self, context, criteria): + LOG.debug("Getting flavor options") + return flavor.create_flavors(criteria) diff --git a/valence/controller/handlers/node_controller.py b/valence/controller/handlers/node_controller.py new file mode 100644 index 0000000..c347081 --- /dev/null +++ b/valence/controller/handlers/node_controller.py @@ -0,0 +1,64 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +from oslo_log import log as logging +from valence.common import osinterface as osapi +from valence.common.redfish import api as rfsapi +import requests + +LOG = logging.getLogger(__name__) + + +class Handler(object): + """Valence Node RPC handler. + + These are the backend operations. They are executed by the backend ervice. + API calls via AMQP (within the ReST API) trigger the handlers to be called. + + """ + + def __init__(self): + super(Handler, self).__init__() + + def list_nodes(self, context, filters): + LOG.info(str(filters)) + return rfsapi.nodes_list(None, filters) + + def get_nodebyid(self, context, nodeid): + return rfsapi.get_nodebyid(nodeid) + + def delete_composednode(self, context, nodeid): + return rfsapi.delete_composednode(nodeid) + + def update_node(self, context, nodeid): + return {"node": "Update node attributes"} + + def compose_nodes(self, context, criteria): + """Chassis details could also be fetched and inserted""" + + # no of nodes to compose + nodes_to_compose = int(criteria["nodes"]) if "nodes" in criteria else 1 + node_criteria = criteria["filter"] if "filter" in criteria else {} + #no of node is not currently implemented + return rfsapi.compose_node(node_criteria) + + def list_node_storages(self, context, data): + return {"node": "List the storages attached to the node"} + + def map_node_storage(self, context, data): + return {"node": "Map storages to a node"} + + def delete_node_storage(self, context, data): + return {"node": "Deleted storages mapped to a node"} diff --git a/valence/flavor/__init__.py b/valence/flavor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/flavor/flavor.py b/valence/flavor/flavor.py new file mode 100644 index 0000000..d4fc7cd --- /dev/null +++ b/valence/flavor/flavor.py @@ -0,0 +1,54 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from importlib import import_module +# from valence.flavor.plugins import * +import os +from oslo_log import log as logging +from valence.common.redfish import api as rfs + +FLAVOR_PLUGIN_PATH = os.path.dirname(os.path.abspath(__file__)) + '/plugins' +logger = logging.getLogger() + + +def get_available_criteria(): + pluginfiles = [f.split('.')[0] + for f in os.listdir(FLAVOR_PLUGIN_PATH) + if os.path.isfile(os.path.join(FLAVOR_PLUGIN_PATH, f)) + and not f.startswith('__') and f.endswith('.py')] + resp = [] + for p in pluginfiles: + module = import_module("valence.flavor.plugins." + p) + myclass = getattr(module, p + 'Generator') + inst = myclass([]) + resp.append({'name': p, 'description': inst.description()}) + return {'criteria': resp} + + +def create_flavors(criteria): + """criteria : comma seperated generator names + + This should be same as thier file name) + + """ + respjson = [] + lst_nodes = rfs.nodes_list() + for g in criteria.split(","): + if g: + logger.info("Calling generator : %s ." % g) + module = __import__("valence.flavor.plugins." + g, fromlist=["*"]) + classobj = getattr(module, g + "Generator") + inst = classobj(lst_nodes) + respjson.append(inst.generate()) + return respjson diff --git a/valence/flavor/generatorbase.py b/valence/flavor/generatorbase.py new file mode 100644 index 0000000..0aedaad --- /dev/null +++ b/valence/flavor/generatorbase.py @@ -0,0 +1,37 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import uuid + + +class generatorbase(object): + def __init__(self, nodes): + self.nodes = nodes + self.prepend_name = 'irsd-' + + def description(self): + return "Description of plugins" + + def _flavor_template(self, name, ram, cpus, disk, extraspecs): + return json.dumps([{"flavor": + {"name": name, + "ram": int(ram), + "vcpus": int(cpus), + "disk": int(disk), + "id": str(uuid.uuid4())}}, + {"extra_specs": extraspecs}]) + + def generate(self): + raise NotImplementedError() diff --git a/valence/flavor/plugins/__init__.py b/valence/flavor/plugins/__init__.py new file mode 100644 index 0000000..b7f932d --- /dev/null +++ b/valence/flavor/plugins/__init__.py @@ -0,0 +1,5 @@ +"""from os.path import dirname, basename, isfile +import glob +modules = glob.glob(dirname(__file__)+"/*.py") +__all__ = [ basename(f)[:-3] for f in modules if isfile(f)] +""" diff --git a/valence/flavor/plugins/assettag.py b/valence/flavor/plugins/assettag.py new file mode 100644 index 0000000..334b2ae --- /dev/null +++ b/valence/flavor/plugins/assettag.py @@ -0,0 +1,46 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import re +from oslo_log import log as logging +from valence.flavor.generatorbase import generatorbase + +LOG = logging.getLogger() + +class assettagGenerator(generatorbase): + def __init__(self, nodes): + generatorbase.__init__(self, nodes) + + def description(self): + return "Demo only: Generates location based on assettag" + + def generate(self): + LOG.info("Default Generator") + for node in self.nodes: + LOG.info("Node ID " + node['nodeid']) + location = node['location'] + location = location.split('Sled')[0] + #Systems:Rack1-Block1-Sled2-Node1_Sled:Rack1-Block1-Sled2_Enclosure:Rack1-Block1_Rack:Rack1_ + location_lst = re.split("(\d+)", location) + LOG.info(str(location_lst)) + location_lst = list(filter(None, location_lst)) + LOG.info(str(location_lst)) + extraspecs = {location_lst[i]: location_lst[i+1] for i in range(0,len(location_lst),2)} + name = self.prepend_name + location + return { + self._flavor_template("L_" + name, node['ram'] , node['cpu']["count"], node['storage'], extraspecs), + self._flavor_template("M_" + name, int(node['ram'])/2 , int(node['cpu']["count"])/2 , int(node['storage'])/2, extraspecs), + self._flavor_template("S_" + name, int(node['ram'])/4 , int(node['cpu']["count"])/4 , int(node['storage'])/4, extraspecs) + } diff --git a/valence/flavor/plugins/default.py b/valence/flavor/plugins/default.py new file mode 100644 index 0000000..ea97e2c --- /dev/null +++ b/valence/flavor/plugins/default.py @@ -0,0 +1,43 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import re +from oslo_log import log as logging +from valence.flavor.generatorbase import generatorbase + +LOG = logging.getLogger() + +class defaultGenerator(generatorbase): + def __init__(self, nodes): + generatorbase.__init__(self, nodes) + + def description(self): + return "Generates 3 flavors(Tiny, Medium, Large) for each node considering all cpu cores, ram and storage" + + def generate(self): + LOG.info("Default Generator") + for node in self.nodes: + LOG.info("Node ID " + node['nodeid']) + location = node['location'] + #Systems:Rack1-Block1-Sled2-Node1_Sled:Rack1-Block1-Sled2_Enclosure:Rack1-Block1_Rack:Rack1_ + location_lst = location.split("_"); + location_lst = list(filter(None, location_lst)) + extraspecs = { l[0] : l[1] for l in (l.split(":") for l in location_lst) } + name = self.prepend_name + location + return { + self._flavor_template("L_" + name, node['ram'] , node['cpu']["count"], node['storage'], extraspecs), + self._flavor_template("M_" + name, int(node['ram'])/2 , int(node['cpu']["count"])/2 , int(node['storage'])/2, extraspecs), + self._flavor_template("S_" + name, int(node['ram'])/4 , int(node['cpu']["count"])/4 , int(node['storage'])/4, extraspecs) + } diff --git a/valence/flavor/plugins/example.py b/valence/flavor/plugins/example.py new file mode 100644 index 0000000..a041476 --- /dev/null +++ b/valence/flavor/plugins/example.py @@ -0,0 +1,27 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from valence.flavor.generatorbase import generatorbase + +logger = logging.getLogger() + + +class exampleGenerator(generatorbase): + def __init__(self, nodes): + generatorbase.__init__(self, nodes) + + def generate(self): + logger.info("Example Flavor Generate") + return {"Error": "Example Flavor Generator- Not Yet Implemented"} diff --git a/valence/objects/__init__.py b/valence/objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/valence/objects/base.py b/valence/objects/base.py new file mode 100644 index 0000000..d829f73 --- /dev/null +++ b/valence/objects/base.py @@ -0,0 +1,63 @@ +# Copyright (c) 2016 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Valence common internal object model""" + +from oslo_versionedobjects import base as ovoo_base +from oslo_versionedobjects import fields as ovoo_fields + + +remotable_classmethod = ovoo_base.remotable_classmethod +remotable = ovoo_base.remotable + + +class ValenceObjectRegistry(ovoo_base.VersionedObjectRegistry): + pass + + +class ValenceObject(ovoo_base.VersionedObject): + """Base class and object factory. + + This forms the base of all objects that can be remoted or instantiated + via RPC. Simply defining a class that inherits from this base class + will make it remotely instantiatable. Objects should implement the + necessary "get" classmethod routines as well as "save" object methods + as appropriate. + """ + OBJ_PROJECT_NAMESPACE = 'Valence' + + def as_dict(self): + return {k: getattr(self, k) + for k in self.fields + if self.obj_attr_is_set(k)} + + +class ValenceObjectDictCompat(ovoo_base.VersionedObjectDictCompat): + pass + + +class ValencePersistentObject(object): + """Mixin class for Persistent objects. + + This adds the fields that we use in common for all persistent objects. + """ + fields = { + 'created_at': ovoo_fields.DateTimeField(nullable=True), + 'updated_at': ovoo_fields.DateTimeField(nullable=True), + } + + +class ValenceObjectSerializer(ovoo_base.VersionedObjectSerializer): + # Base class to use for object hydration + OBJ_BASE_CLASS = ValenceObject diff --git a/valence/tests/__init__.py b/valence/tests/__init__.py new file mode 100644 index 0000000..2f6e1f9 --- /dev/null +++ b/valence/tests/__init__.py @@ -0,0 +1,24 @@ +import os +from pecan import set_config +from pecan.testing import load_test_app +from unittest import TestCase + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """Functional Test Class + + Used for functional tests where you need to test your + literal application and its integration with the framework. + + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/valence/tests/config.py b/valence/tests/config.py new file mode 100644 index 0000000..80f0933 --- /dev/null +++ b/valence/tests/config.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# Server Specific Configurations +server = { + 'port': '8080', + 'host': '0.0.0.0' +} + +# Pecan Application Configurations +app = { + 'root': 'valence.controllers.root.RootController', + 'modules': ['valence'], + 'static_root': '%(confdir)s/../../public', + 'template_path': '%(confdir)s/../templates', + 'debug': True, + 'errors': { + '404': '/error/404', + '__force_dict__': True + } +} + +# Custom Configurations must be in Python dictionary format:: +# +# foo = {'bar':'baz'} +# +# All configurations are accessible at:: +# pecan.conf diff --git a/valence/tests/test_functional.py b/valence/tests/test_functional.py new file mode 100644 index 0000000..33b8321 --- /dev/null +++ b/valence/tests/test_functional.py @@ -0,0 +1,22 @@ +from valence.tests import FunctionalTest +# from unittest import TestCase +# from webtest import TestApp + + +class TestRootController(FunctionalTest): + + def test_get(self): + response = self.app.get('/') + assert response.status_int == 200 + + def test_search(self): + response = self.app.post('/', params={'q': 'RestController'}) + assert response.status_int == 302 + assert response.headers['Location'] == ( + 'http://pecan.readthedocs.org/en/latest/search.html' + '?q=RestController' + ) + + def test_get_not_found(self): + response = self.app.get('/a/bogus/url', expect_errors=True) + assert response.status_int == 404 diff --git a/valence/tests/test_units.py b/valence/tests/test_units.py new file mode 100644 index 0000000..573fb68 --- /dev/null +++ b/valence/tests/test_units.py @@ -0,0 +1,7 @@ +from unittest import TestCase + + +class TestUnits(TestCase): + + def test_units(self): + assert 5 * 5 == 25 diff --git a/valence/ui/README.md b/valence/ui/README.md new file mode 100644 index 0000000..d9afdec --- /dev/null +++ b/valence/ui/README.md @@ -0,0 +1,44 @@ +Rack Scale Design (RSD) Web UI +============================== + +The `ui` folder contains HTML, JavaScript and CSS code for a Web UI that can be used to explore Rack Scale Design (RSD) artifacts and compose/disassemble nodes. + +##Pre-reqs +1. Install Node and NPM using the OS-specific installer on +2. Update npm to the latest verions + ``` + sudo npm install npm -g + ``` +3. Follow the instructions in the docs directory for setting up the apache ui-proxy. + +##Install +1. `cd` to the `ui` directory and run: + ``` + npm install + ``` + * This will install all packages listed in `package.json` file. + * If you are adding a new package dependency, make sure to save it to the `package.json` file. You can install the package and update `package.json` in a single command: `npm install --save new-package@6.2.5` + * This installs the webpack dev server which can be used for serving the Web UI during development. + +##Run +1. Build + ``` + npm run build + ``` +2. Start webpack-dev-server in watch mode on the `src` dir: + ``` + npm run devserver + ``` + * The `devserver` command is defined in `package.json`. It launches the `webpack-dev-server` program in `hot` mode and watches the `src` directory. If you make any changes to any file in the `src` dir, `webpack-dev-server` compiles everything to a temp location and reloads the display page (`index.html`). + +3. Open browser and goto to view the UI + +##Develop +1. The `src\index.html` is the root HTML page for the Web UI. It has a `div` element called `app` which is where the dynamic UI contents get inserted. The file `src/js/main.js` does this insertion using: + ``` + ReactDOM.render(, document.getElementById('app')); + ``` + The root of the app content is provided by the React component `src/js/components/Layout.js`. It wraps others components Pods.js, Racks.js, etc which encapsulate the state and rendering details of Pods, Rack, etc respectively. +2. The file `webpack.config.js` contains loaders that transpile React components to plain JavaScript that any browser can understand. The command `webpack` (`package.json` contains `dev-build` and `build` commands which can be used instead via `npm run `) kicks off this transpilation process. +3. Modify appropriate files and use the devserver detailed above to test your changes. + diff --git a/valence/ui/package.json b/valence/ui/package.json new file mode 100644 index 0000000..24d929d --- /dev/null +++ b/valence/ui/package.json @@ -0,0 +1,61 @@ +{ + "name": "rsd-webui", + "version": "0.1.0", + "description": "Web UI to explore Rack Scale Design (RSD) artifacts and compose/disassemble nodes.", + "main": "src/main.js", + "keywords": [ + "rsd", + "UI", + "compose", + "disassemble" + ], + "dependencies": { + "bootstrap-sass": "^3.3.6", + "jquery": "^3.1.0", + "react": "^0.14.6", + "react-dom": "^0.14.6" + }, + "devDependencies": { + "babel-core": "^6.4.5", + "babel-loader": "^6.2.0", + "babel-plugin-add-module-exports": "^0.1.2", + "babel-plugin-react-html-attrs": "^2.0.0", + "babel-plugin-transform-class-properties": "^6.3.13", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "bootstrap-loader": "^1.1.0", + "css-loader": "^0.23.1", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "imports-loader": "^0.6.5", + "node-sass": "^3.8.0", + "resolve-url-loader": "^1.6.0", + "sass-loader": "^4.0.0", + "style-loader": "^0.13.1", + "url-loader": "^0.5.7", + "webpack": "^1.13.1", + "webpack-dev-server": "^1.14.1" + }, + "scripts": { + "devserver": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --progress --colors --content-base src --inline --hot --host 0.0.0.0", + "dev-build": "NODE_ENV=development webpack --progress --colors", + "build": "NODE_ENV=production webpack --progress --colors", + "packages": "npm list --depth=0", + "package:purge": "rm -rf node_modules", + "package:reinstall": "npm run package:purge && npm install", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "contributors": [ + { + "name": "Deepti Ramakrishna", + "email": "deepti.ramakrishna@intel.com" + }, + { + "name": "Lin Yang", + "email": "lin.a.yang@intel.com" + } + ], + "license": "Apache-2.0" +} diff --git a/valence/ui/src/customized.css b/valence/ui/src/customized.css new file mode 100644 index 0000000..49beb6a --- /dev/null +++ b/valence/ui/src/customized.css @@ -0,0 +1,163 @@ +/* + * Base structure + */ + +/* Move down content because we have a fixed navbar that is 50px tall */ +body { + padding-top: 50px; +} + +th, td { + padding: 10px; +} + +/* + * Global add-ons + */ + +.sub-header { + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +/* + * Top navigation + * Hide default border to remove 1px line. + */ +.navbar-fixed-top { + border: 0; +} + +/* + * Dashboard + */ +.dashboard { +/* padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; +*/ + border-radius: 4px; + background-color: #f8f8f8; +} + +.dashboard .row{ + margin-left: auto; + margin-right: auto; +} + +hr.separator { + background-color: #c0c0c0; + height: 2px; +} + +.detail-button { + background-color: #428bca; + border: none; + color: white; + padding: 6px 18px; + margin-right: 6px; + text-align: center; + display: block; + border-radius: 4px; + float: right; +} + +.compose-button { + background-color: #428bca; + border: none; + color: white; + padding: 10px 24px; + text-align: center; + margin-top: 10px; + display: block; + border-radius: 4px; + position: relative; + float: left; +} + +.details { + margin-top: 30px; + padding: 20px; + width: 100%; + border-radius: 4px; + background-color: #f8f8f8; +} + +/* + * Sidebar + */ +.sidebar { + position: relative; + background-color: #eeeeee; + border-radius: 4px; + +/* top: 0px;*/ +/* bottom: 20;*/ +/* left: 0;*/ +/* z-index: 1000;*/ +/* display: block;*/ +/* padding: 20px;*/ +/* overflow-x: hidden;*/ +/* overflow-y: auto;*/ +/* background-color: #f5f5f5;*/ +/* border-right: 1px solid #eee;*/ + color: inherit; +} + +/* Sidebar navigation */ +.nav-sidebar { +/* margin-right: -21px; + margin-bottom: 20px;*/ + margin-left: -15px; + margin-right: -15px; +} +.nav-sidebar > li > a { + padding-right: 20px; + padding-left: 20px; +} +.nav-sidebar > .active > a, +.nav-sidebar > .active > a:hover, +.nav-sidebar > .active > a:focus { + color: #fff; + background-color: #428bca; + border-radius: 4px; +} + + +/* + * Main content + */ + +.main { + padding: 20px; +} +@media (min-width: 768px) { + .main { + padding-right: 40px; + padding-left: 40px; + } +} +.main .page-header { + margin-top: 0; +} + + +/* + * Placeholder dashboard ideas + */ + +.placeholders { + margin-bottom: 30px; + text-align: center; +} +.placeholders h4 { + margin-bottom: 0; +} +.placeholder { + margin-bottom: 20px; +} +.placeholder img { + display: inline-block; + border-radius: 50%; +} + diff --git a/valence/ui/src/index.html b/valence/ui/src/index.html new file mode 100644 index 0000000..bde32af --- /dev/null +++ b/valence/ui/src/index.html @@ -0,0 +1,16 @@ + + + + + Rack Scale Design + + + + + +
+ + + + + diff --git a/valence/ui/src/js/components/Layout.js b/valence/ui/src/js/components/Layout.js new file mode 100644 index 0000000..8ceaa2d --- /dev/null +++ b/valence/ui/src/js/components/Layout.js @@ -0,0 +1,144 @@ +import React from "react"; +import ComposeDisplay from "./home/ComposeDisplay"; +import DetailDisplay from "./home/DetailDisplay"; +import Home from "./home/Home"; + +const Layout = React.createClass({ + + getInitialState: function() { + return { + homeDisplay: "inline-block", + detailDisplay: "none", + composeDisplay: "none", + detailData: "", + pods: [], + racks: [], + systems: [], + storage: [], + nodes: [] + }; + }, + + displayHome: function() { + this.setState({ + homeDisplay: "inline-block", + detailDisplay: "none", + composeDisplay: "none", + detailData: "" + }); + }, + + displayDetail: function(item) { + this.setState({ + homeDisplay: "none", + detailDisplay: "inline-block", + composeDisplay: "none", + detailData: JSON.stringify(item, null, "\t") + }); + }, + + displayCompose: function() { + this.setState({ + homeDisplay: "none", + detailDisplay: "none", + composeDisplay: "inline-block", + detailData: "" + }); + }, + + updatePods: function(pods) { + this.setState({pods: pods}); + }, + + updateRacks: function(racks) { + this.setState({racks: racks}); + }, + + updateSystems: function(systems) { + this.setState({systems: systems}); + }, + + updateStorage: function(storage) { + this.setState({storage: storage}); + }, + + updateNodes: function(nodes) { + this.setState({nodes: nodes}); + }, + + render: function() { + return ( +
+ + + + + + +
+
+

Version: 0.1

+
+
+
+ ); + } +}); + +export default Layout; diff --git a/valence/ui/src/js/components/home/ComposeDisplay.js b/valence/ui/src/js/components/home/ComposeDisplay.js new file mode 100644 index 0000000..f7b6fbc --- /dev/null +++ b/valence/ui/src/js/components/home/ComposeDisplay.js @@ -0,0 +1,122 @@ +import React from "react"; + +var config = require('../../config.js'); +var util = require('../../util.js'); + +const ComposeDisplay = React.createClass({ + + getInitialState: function() { + return { + processors: [] + }; + }, + + componentDidMount() { + this.getProcessors(); + }, + + compose: function() { + var data = this.prepareRequest(); + var url = config.url + '/redfish/v1/Nodes/Actions/Allocate'; + $.ajax({ + url: url, + type: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + data: data, + dataType: 'text', + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); + this.clearInputs() + this.props.onHideCompose(); + }, + + getProcessors: function() { + util.getProcessors(this.props.systemList, this.setProcessors); + }, + + setProcessors: function(processors) { + this.setState({processors: processors}); + this.fillForms(); + }, + + fillForms: function() { + var sel = document.getElementById('procModels'); + sel.innerHTML = ""; + for (var i = 0; i < this.state.processors.length; i++) { + if (this.state.processors[i]['Model']) { + var opt = document.createElement('option'); + opt.innerHTML = this.state.processors[i]['Model']; + opt.value = this.state.processors[i]['Model']; + sel.appendChild(opt); + } + } + }, + + prepareRequest: function() { + var name = document.getElementById('name').value; + var description = document.getElementById('description').value; + var totalMem = document.getElementById('totalMem').value; + var procModel = document.getElementById('procModels').value; + if (procModel == "") { + procModel = null; + } + var data = { + "Name": name, + "Description": description, + "Memory": [{ + "CapacityMiB": totalMem * 1000 + }], + "Processors": [{ + "Model": procModel + }] + } + return JSON.stringify(data); + }, + + clearInputs: function() { + document.getElementById("inputForm").reset(); + }, + + render: function() { + return ( +
+
+ + + + + + + + + + + + + + + + + + + +
Name:
Description:
System Memory GB:
Processor Model:
+
+ this.compose()} value="Compose" /> + this.props.onHideCompose()} value="Return" /> +
+ ); + } + +}); + +export default ComposeDisplay diff --git a/valence/ui/src/js/components/home/DetailDisplay.js b/valence/ui/src/js/components/home/DetailDisplay.js new file mode 100644 index 0000000..9504da7 --- /dev/null +++ b/valence/ui/src/js/components/home/DetailDisplay.js @@ -0,0 +1,17 @@ +import React from "react"; + +const DetailDisplay = React.createClass({ + + render: function() { + return ( +
+
{this.props.data}
+ this.props.onHideDetail()} value="Return" /> +
+ ); + } +}); + +export default DetailDisplay diff --git a/valence/ui/src/js/components/home/Home.js b/valence/ui/src/js/components/home/Home.js new file mode 100644 index 0000000..905fa7a --- /dev/null +++ b/valence/ui/src/js/components/home/Home.js @@ -0,0 +1,153 @@ +import React from "react"; +import ResourceList from "./ResourceList"; +import NodeList from "./NodeList"; + +var config = require('../../config.js'); +var util = require('../../util.js'); + +const Home = React.createClass({ + + configCompose: function() { + /* This is a temporary function that will compose a node based on the JSON value + * of the nodeConfig variable in config.js. + * + * TODO(ntpttr): Remove this once the compose menu is fully flushed out. + */ + var url = config.url + '/redfish/v1/Nodes/Actions/Allocate'; + $.ajax({ + url: url, + type: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(config.nodeConfig), + dataType: 'text', + success: function(resp) { + this.getNodes(); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); + }, + + componentWillMount: function() { + this.getPods(); + this.getRacks(); + this.getSystems(); + this.getStorage(); + this.getNodes(); + }, + + getPods: function() { + util.getPods(this.setPods); + }, + + getRacks: function() { + util.getRacks(this.setRacks); + }, + + getSystems: function() { + util.getSystems(this.setSystems); + }, + + getStorage: function() { + util.getStorage(this.setStorage); + }, + + getNodes: function() { + util.getNodes(this.setNodes); + }, + + setPods: function(pods) { + this.props.onUpdatePods(pods); + }, + + setRacks: function(racks) { + this.props.onUpdateRacks(racks); + }, + + setSystems: function(systems) { + this.props.onUpdateSystems(systems); + }, + + setStorage: function(storage) { + this.props.onUpdateStorage(storage); + }, + + setNodes: function(nodes) { + this.props.onUpdateNodes(nodes); + }, + + render: function() { + return ( +
+
+

Welcome to RSD Details

+

This is a brief overview of all kinds of resources in this environment. See the User Guide for more information on how to configure them.

+

+ this.props.onShowCompose()} value="Compose Node" /> + this.configCompose()} value="Compose From Config File" /> +

+
+ + +
+ ); + } +}); + +export default Home diff --git a/valence/ui/src/js/components/home/NodeList.js b/valence/ui/src/js/components/home/NodeList.js new file mode 100644 index 0000000..f030db5 --- /dev/null +++ b/valence/ui/src/js/components/home/NodeList.js @@ -0,0 +1,86 @@ +import React from "react"; + +var config = require('../../config.js'); +var util = require('../../util.js'); + +const NodeList = React.createClass({ + + delete: function(nodeId) { + var url = config.url + '/redfish/v1/Nodes/' + nodeId; + $.ajax({ + url: url, + type: 'DELETE', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + success: function(resp) { + this.props.onUpdateNodes(); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); + }, + + assemble: function(nodeId) { + var url = config.url + '/redfish/v1/Nodes/' + nodeId + '/Actions/ComposedNode.Assemble' + $.ajax({ + url: url, + type: 'POST', + success: function(resp) { + this.props.onUpdateNodes(); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); + }, + + powerOn: function(nodeId) { + var url = config.url + '/redfish/v1/Nodes/' + nodeId + '/Actions/ComposedNode.Reset' + console.log(nodeId); + $.ajax({ + url: url, + type: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + data: JSON.stringify({"ResetType": "On"}), + success: function(resp) { + console.log(resp); + this.props.onUpdateNodes(); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); + }, + + renderList: function() { + return this.props.nodes.map((node, i) => +
+ {node.Name} + this.props.onShowDetail(node)} value="Show" /> + this.delete(node.Id)} value="Delete" /> + this.assemble(node.Id)} value="Assemble" /> + this.powerOn(node.Id)} value="Power On" /> +
+ {node.Description} +
+
+ ); + }, + + render: function() { + return ( +
+ {this.renderList()} +
+ ); + }, +}); + +NodeList.defaultProps = { nodes: [], header: ""}; + +export default NodeList; diff --git a/valence/ui/src/js/components/home/ResourceList.js b/valence/ui/src/js/components/home/ResourceList.js new file mode 100644 index 0000000..7a6725f --- /dev/null +++ b/valence/ui/src/js/components/home/ResourceList.js @@ -0,0 +1,30 @@ +import React from "react"; + +var util = require('../../util.js'); + +const ResourceList = React.createClass({ + + renderList: function() { + return this.props.resources.map((resource, i) => +
+ {resource.Name} + this.props.onShowDetail(resource)} value="Show" /> +
+ {resource.Description} +
+
+ ); + }, + + render: function() { + return ( +
+ {this.renderList()} +
+ ); + }, +}); + +ResourceList.defaultProps = { resources: [], header: ""}; + +export default ResourceList; diff --git a/valence/ui/src/js/config.js b/valence/ui/src/js/config.js new file mode 100644 index 0000000..33408d3 --- /dev/null +++ b/valence/ui/src/js/config.js @@ -0,0 +1,19 @@ +/* + * Configuration file for RSC UI. + */ + +exports.url = "http://127.0.0.1:6000" + +exports.nodeConfig = +{ + "Name": "Test Node", + "Description": "This is a node composed from the config file.", + "Processors": [{ + "Model": null + }], + "Memory": [{ + "CapacityMiB": 8000 + }], + "LocalDrives": null +} + diff --git a/valence/ui/src/js/main.js b/valence/ui/src/js/main.js new file mode 100644 index 0000000..3bce880 --- /dev/null +++ b/valence/ui/src/js/main.js @@ -0,0 +1,6 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +import Layout from "./components/Layout"; + +ReactDOM.render(, document.getElementById('app')); diff --git a/valence/ui/src/js/util.js b/valence/ui/src/js/util.js new file mode 100644 index 0000000..3ad1de1 --- /dev/null +++ b/valence/ui/src/js/util.js @@ -0,0 +1,143 @@ +var config = require('./config.js'); +var util = require('./util.js'); + +exports.getPods = function(callback) { + var url = config.url + '/redfish/v1/Chassis'; + $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + cache: false, + success: function(resp) { + var chassis = this.listItems(resp['Members']); + var pods = this.filterChassis(chassis, 'Pod'); + callback(pods); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); +}; + +exports.getRacks = function(callback) { + var url = config.url + '/redfish/v1/Chassis'; + $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + cache: false, + success: function(resp) { + var chassis = this.listItems(resp['Members']); + var racks = this.filterChassis(chassis, 'Rack'); + callback(racks); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); +}; + +exports.getSystems = function(callback) { + var url = config.url + '/redfish/v1/Systems'; + $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + cache: false, + success: function(resp) { + var systems = this.listItems(resp['Members']); + callback(systems); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); +}; + +exports.getNodes = function(callback) { + var url = config.url + '/redfish/v1/Nodes'; + $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + cache: false, + success: function(resp) { + var nodes = this.listItems(resp['Members']); + callback(nodes); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); +}; + +exports.getStorage = function(callback) { + var url = config.url + '/redfish/v1/Services/1/LogicalDrives'; + $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + cache: false, + success: function(resp) { + var drives = this.listItems(resp['Members']); + callback(drives); + }.bind(this), + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + }.bind(this) + }); +}; + +exports.getProcessors = function(systems, callback) { + var processors = []; + var systemProcessorIds; + var systemProcessors; + for (var i = 0; i < systems.length; i++) { + systemProcessorIds = util.readAndReturn(systems[i]['Processors']['@odata.id']); + systemProcessorIds = JSON.parse(systemProcessorIds); + systemProcessors = util.listItems(systemProcessorIds['Members']); + for (var j = 0; j < systemProcessors.length; j++) { + processors.push(systemProcessors[j]); + } + } + callback(processors); +}; + +exports.listItems = function(items) { + var returnItems = []; + var count = items.length; + var resource; + var itemJson; + var itemJsonObj; + for (var i=0; i