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 0000000..bc33aea Binary files /dev/null and b/doc/api/VALENCE_API-v0.4.1.docx differ 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