Git Product home page Git Product logo

redfish-interface-emulator's Issues

Using for static mockups

I am really new to this so please be kind...
I am seeing the following error when I try to start the Emulator "Traceback (most recent call last):
File "emulator.py", line 20, in
import g
File "/data/Redfish-Interface-Emulator/g.py", line 11, in
from flask import Flask
ImportError: No module named flask"

Any guidance would be appreciated!
Thanks.

URL behaviors are different for static and dynamic resources

Using the Restlet Client, which does not do automatic redirection, the URLs that must be used for accessing static and dynamic emulator resources are different.

Dynamic resources must be accessed with URLs that do NOT end with a "/" character. If the URL for a dynamic resource ends with a "/" character, the emulator gives a "500 INTERNAL SERVER ERROR" response.

Static resources must be accessed with URLS that DO end with a "/" character. If the URL for a static resource does NOT end with a "/" character, the emulator gives a "301 MOVED PERMANENTLY" response, which is a redirect.

ServiceEnabled boolean being returned as a string

There are several objects that have a ServiceEnabled boolean property. Most are correctly returning this as it's defined as a boolean, but the following are returning the string "true" instead:

  • EventService
  • CompositionService
  • Manager

sample redfish emulator implementing .is it correct?

from flask import Flask
from flask_restful import Resource, Api
import sys, traceback
import copy
import strgen
from utils import replace_recurse

from Acclipc.mdc_webtask import *

BASE_PATH = "/redfish/v1/"

app = Flask(name)
api = Api(app)

class ResourceCollection(Resource):
def init(self):
self.rest_base = BASE_PATH
self.uuid = ""

def get(self):
	"""
	Configuration property - Service Root
	"""
	config = {
		'@odata.context': self.rest_base + '$metadata#ServiceRoot',
		'@odata.type': '#ServiceRoot.1.0.0.ServiceRoot',
		'@odata.id': self.rest_base,
		'Id': 'RootService',
		'Name': 'Root Service',
		'ServiceVersion': '1.0.0',
		'UUID': self.uuid,
		'Links': {
			'Chassis': {'@odata.id': self.rest_base + 'Chassis'}
		}
	}

	return config

class Chassis(Resource):

def __init__(self):
	self.rb = BASE_PATH
	self.config = {
		'@odata.context': self.rb + '$metadata#ChassisCollection.ChassisCollection',
		'@odata.id': self.rb + 'Chassis',
		'@odata.type': '#ChassisCollection.1.0.0.ChassisCollection',
		'Name': 'Chassis Collection',
		'[email protected]': 1,
		'Member' : self.rb + 'Chassis/Chassis-1',
		}	
		
def get(self):
	try:
		resp = self.config, 200
	except Exception:
		traceback.print_exc()
		resp = INTERNAL_ERROR
	return resp

class Chassisinfo(Resource):

def __init__(self):
	self.rb = BASE_PATH
	self.config = {
		"@odata.context": self.rb +"$metadata#Chassis.Chassis",
		"@odata.id": self.rb + "Chassis",
		"@odata.type": "#Chassis.v1_0_0.Chassis",
		'Name': 'MDC System Chassis',
		"ChassisType": "RackMount",
		"SerialNumber": "",
		"PartNumber": "",
		"MaxPower": "",
		"Chassis ID": "",
		"AssetTag": "",
		"Oem": {
			"Links": {
				"Cartridge": [ 
					{
						"@odata.id": self.rb +"Chassis/Chassis-1/Cartridge"
					}
				],
				"Switch": [ 
					{
						"@odata.id": self.rb +"Chassis/Chassis-1/Switch"
					}
				]
			}
		}
		}
		
def get(self,ident):
	global wildcards
	res = login("accl","accl")
	result = chassisinfo()
	self.config['@odata.id']= self.rb + ident
	self.config['PartNumber']= result[0]
	self.config['SerialNumber']= result[1]
	self.config['MaxPower']= result[2]
	self.config['Chassis ID']= result[3]
	self.config['AssetTag']= result[4]
	return self.config, 200

class CartridgeCollection(Resource):

def __init__(self):
	self.rb = BASE_PATH
	self.cartridge = "Cartridge"
	self.switch = "Switch"
	self.config = {
		'@odata.context': self.rb + '$metadata#cartridgeCollection.cartridgeCollection',
		'@odata.id': self.rb + 'Cartridge',
		'@odata.type': '#cartridgeCollection.1.0.0.cartridgeCollection',
		'Name': 'Cartridge Collection',
		'Links': {}
	}
	self.config['Links']['[email protected]'] = ''
	self.config['Links']['Members'] = ''
	
def get(self,ident1,ident2):
	if ident2 == self.cartridge:
		self.config['Name']=self.cartridge + " Collection"
		self.config['Links']['[email protected]'] = 12
		self.config['Links']['Members'] = ''
		return self.config, 200
	elif ident2 == self.switch:
		self.config['Name']=self.switch + " Collection"
		self.config['Links']['[email protected]'] = 2
		self.config['Links']['Members'] = ''
		return self.config, 200

api.add_resource(ResourceCollection, '/redfish/v1')
api.add_resource(Chassis, '/redfish/v1/Chassis')
api.add_resource(Chassisinfo, '/redfish/v1/Chassis/string:ident')
api.add_resource(CartridgeCollection, '/redfish/v1/Chassis/string:ident1/string:ident2')

if name == 'main':
app.run('0.0.0.0', debug=True)

Request to create a Python module for this tool

Currently, SNIA is extending code base to become the Swordfish Interface Emulator. The instructions for putting the packages together is getting complex. It would be simpler if ‘pip redfish-emul’ could be run, followed by ‘pip swordfish-emul’.

Collection POST command fails to add a resource instance to the ComputerSystem collection

A POST command to add a new instance to the ComputerSystem collection fails with an error message that says “KeyError: ‘Links’”. This happens no matter what is in the body of the POST command.

After reviewing the pull request history (see #16 ), this appears to be a consequence of the way support for composed systems was added to the ComputerSystem dynamic resource. Perhaps that work was not completed?

Populating should be optional

The code in emulator.py seems to assume that ./infra_gen/populate_config.json exists. The populate step should be options (come up empty if desired). Options to do this are:

  1. "POPULATE" Property is set to "emulator" in emulator_config.json
  2. Existence of ./infra_gen/populate_config.json
    # Calls INFRAGEN and populates emulator according to populate-config.json
    with open(INFRAGEN_CONFIG, 'r') as f:
        infragen_config = json.load(f)
    populate(infragen_config.get('POPULATE',10))

unittests.py was broken

The file unittests.py has two minor Python syntax issues raised if we try to run it:

  1. File "unittests.py", line 62
    logger.info('PASS: GET of {0} successful (response below)\n {1}'.format(getting, r.text))
    ^
    TabError: inconsistent use of tabs and spaces in indentation

  2. File "unittests.py", line 156
    print'Testing interface at:', sys.argv[2]
    ^
    SyntaxError: invalid syntax

GET on EventService works, but causes an error

Running the unmodified code from master, the emulator outputs an error on the console whenever EventService is accessed with a normal GET:

GET http://localhost:5000/redfish/v1/EventService

The Restlet Client receives a "200 OK" result and an OK-seeming Body from this GET, but the following traceback and error message appears on the console:

Traceback (most recent call last):
File "C:\Users\ddon\Documents\EmRf\api_emulator\redfish\EventService_api.py", line 33, in init
g.api.add_resource(SubscriptionCollectionAPI, '/redfish/v1/EventService/Subscriptions')
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask_restful_init_.py", line 404, in add_resource
self.register_view(self.app, resource, *urls, **kwargs)
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask_restful_init
.py", line 470, in _register_view
app.add_url_rule(rule, view_func=resource_func, **kwargs)
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask\app.py", line 64, in wrapper_func
return f(self, *args, **kwargs)
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask\app.py", line 1051, in add_url_rule
'existing endpoint function: %s' % endpoint)
AssertionError: View function mapping is overwriting an existing endpoint function: subscriptioncollectionapi

Issues with Using Created Mockup to use Static Emulator

I created my own mockup with a live redfish service from the Redfish-Mockup-Creator and Redfish-Mockup-Server runs as expected. However, when I follow the instructions to use my own mockup with the Redfish-Interface-Emulator it doesn't see my mockup that I'm trying to use. Replacing the \api_emulator\redfish\static directory with my mockup doesn't work. The readme and instructions found in /doc seem to be a little inconsistent, and I'm trying to find out what I'm doing wrong.

Trouble using "/" at the end of a URI

Using a "/" at the end of a URI makes a difference with the emulator, depending upon whether it is for a leaf node (one at the edge of the tree) or a non-leaf node (one inside the tree).

For example, this is what happens when accessing a leaf node:

GET http://localhost:5000/redfish/v1/Systems/CS_5 results in a 200 OK response and produces reasonable output, but...

GET http://localhost:5000/redfish/v1/Systems/CS_5/ gets a 500 Internal Server Error response and the emulator console reports an exception originating in emulator.py.

However, this is what happens when accessing a non-leaf node:

GET http://localhost:5000/redfish/v1/Systems results in a 301 Moved Permanently response, and...

GET http://localhost:5000/redfish/v1/Systems/ results in a 200 OK response and produces reasonable output.

Note that the emulator behavior is different for getting a 200 OK response, depending upon whether the URI is for a leaf node or a non-leaf node.

This behavior is made more obvious by the Chrome Restlet Client extension. By default, Restlet does not automatically follow redirects, so if you leave the trailing "/" off for a non-leaf node, you'll get a 301 Moved Permanently response. Using the same URI with a trailing "/" (or telling Restlet to follow redirects) gets a 200 OK response. But having a trailing "/" on a leaf node causes an exception.

The Chrome Advance REST Client (ARC) seems to silently follow redirects, so there's no obvious difference between having or not having a trailing "/" on a URI for a non-leaf node. But having a trailing "/" on a leaf node still causes an exception.

Should the emulator be showing these differences between having and not having a trailing "/" on URIs?

Getting /redfish/v1/Systems results in "Internal System Error"

(From Don Deel) Everything looks good at first. Using a browser (or the Restlet Client in Chrome) I can see the Redfish service root and access things like the Chassis and Managers collections. But no matter what I do, attempting to access Systems results in an “Internal Server Error” message. The emulator itself keeps running, the same way it would if I simply mis-typed something.

Getting error message “AssertionError: View function mapping is overwriting an existing endpoint function

Accessing /redfish/v1/EventService produces the expected output, but causes an error message.

Running in Redfish mode
Traceback (most recent call last):
File "C:\Users\ddon\Documents\EmRf\api_emulator\redfish\EventService_api.py", line 33, in init
g.api.add_resource(SubscriptionCollectionAPI, '/redfish/v1/EventService/Subscriptions')
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask_restful_init_.py", line 404, in add_resource
self.register_view(self.app, resource, *urls, **kwargs)
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask_restful_init
.py", line 470, in _register_view
app.add_url_rule(rule, view_func=resource_func, **kwargs)
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask\app.py", line 64, in wrapper_func
return f(self, *args, **kwargs)
File "C:\Users\ddon\Documents\EmRf\lib\site-packages\flask\app.py", line 1051, in add_url_rule
'existing endpoint function: %s' % endpoint)
AssertionError: View function mapping is overwriting an existing endpoint function: subscriptioncollectionapi

Restricting the creation of Composed Systems to valid previously created resources?

Hi,

I'm new to the Redfish API and maybe I'm misunderstanding the proposal of the emulator. There is a way to only allow the creation of the new system with valid resources?

From the tests I performed, the Emulator doesn't do any validation against the ResourceDictionary created when the emulator starts. It seems the initial POPULATE is used only to provide some initial examples and not to initialize which will be the valid infrastructure ...

I was expecting that in a POST to CreateGenericComputerSystem, I was enforced to provide a valid existing configuration (payload). Without this validation I was able to create systems with invalid NumberOfProcessors, for example, or any other invalid resource.

I'm misunderstanding the concept of the emulator? There is a way to do this check if a configuration is valid during the POST?

I'm trying to use it as a datacenter emulator of available resources to play (evaluate) different allocation policies of these resources, but to do that I can't have an allocation of an invalid resource. Maybe I need to create an intermediate layer to validate with the GET if the Composed system has only valid existing resources ...

Thank you!

How to change the ComputerSystem.Status property when performing a ResetAction

The emulation code for the ./Systems/{id}/ResetAction resource in located in the file ./api_emulator/redfish/ComputerSystem/ResetAction.py. A behavior of a reset action is to change the ComputerSystem.Status.State property. However, this requires the code import a module within the ComputerSystem.py code in the parent directory. Python 3.3 removed relative imports, so imports from a parent directory requires hacking PYTHONPATH or adding the parent-path to sys.path.

There is a request for a cleaner way.

Need hurdle/regression test suite

Create a rendition of unittest.py to act as hurdle/regression for changes to the emulator. It should test the basic functionality:

  • GET a resource (collection and singleton)
  • PATCH a resource (collection and singleton)
  • POST to a collection resource (to create a new singleton)
  • DELETE a singleton resource
  • Perform on Chassis, Systems, Managers

FileNotFoundError: [Errno 2] No such file or directory: 'Flask-RESTful-0.3.1.zip'

I got the following error when I run install.py
FileNotFoundError: [Errno 2] No such file or directory: 'Flask-RESTful-0.3.1.zip'


I installed Flask-RESTful-0.3.1 using pip, which gives the following error;
No matching distribution found for Flask-RESTful-0.3.1

if I use "pip install -r requirements.txt" command, it adds the following directory:
/usr/lib64/python3.6/site-packages/flask_restful
its version is 3.6.
and it does not contain any zip file.

I also checked:
https://github.com/flask-restful/flask-restful
It does not contain version 0.3.1

Errors in error handling for getting invalid resource

If a request to a resource is made that is invalid or no longer there, looking up the resource will hit a KeyError. This should be fine, but the error handling for this attempts to get the exception information by accessing the 'message' property. Not all exceptions have a message property, including KeyError, so this causes the following:

Traceback (most recent call last):
File "emulator.py", line 180, in get
config = self.get_configuration(resource_manager, path)
File "emulator.py", line 282, in get_configuration
raise PathError("Resource not found: " + str(e.message))
AttributeError: 'KeyError' object has no attribute 'message'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/Users/smcginnis/repos/SNIA/Swordfish/venv/lib/python3.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
rv = self.dispatch_request()
File "/Users/smcginnis/repos/SNIA/Swordfish/venv/lib/python3.7/site-packages/flask/app.py", line 1799, in dispatch_request
return self.view_functionsrule.endpoint
File "/Users/smcginnis/repos/SNIA/Swordfish/venv/lib/python3.7/site-packages/flask_restful/init.py", line 458, in wrapper
resp = resource(*args, **kwargs)
File "/Users/smcginnis/repos/SNIA/Swordfish/venv/lib/python3.7/site-packages/flask/views.py", line 88, in view
return self.dispatch_request(*args, **kwargs)
File "/Users/smcginnis/repos/SNIA/Swordfish/venv/lib/python3.7/site-packages/flask_restful/init.py", line 573, in dispatch_request
resp = meth(*args, **kwargs)
File "emulator.py", line 189, in get
resp = INTERNAL_ERROR_Get
NameError: name 'INTERNAL_ERROR_Get' is not defined

In addition to the e.message issue, there appears to be a typo with INTERNAL_ERROR_Get. We only have an INTERNAL_ERROR variable defined, and it would appear this is what is supposed to be used there.

Remove emulator_ssl.py

The HTTPS functionality in this file was added to emulator.py. The functionality is controlled by the HTTPS property in emulator-config.json. Hence this file should be remove to avoid confusion.

Malformed Status struct in several objects

The Status struct is defined as being:

{ "Health": "Ok", "HealthRollup": "Ok", "Oem": "foo", "State": "Enabled" }

http://redfish.dmtf.org/schemas/v1/Resource.json

There are several emulated objects that incorrectly introduce an additional layer of a "State" key for the status the looks like:

{ "State": { "Health": "Ok", "HealthRollup": "Ok", "Oem": "foo", "State": "Enabled" } }

The following are examples of this incorrect format:

  • ethernetinterfaces
  • memory
  • simplestorage

All of these are defined as having a Status property of type Status.

Correct examples can be found in:

  • processor
  • thermal
  • power

And several other locations.

Incorrect "dummy" values in static mockup for processors

I was looking at the contents of /redfish/v1/Systems/System-/Processors/CPU in the static mockup. I think it would be helpful if the CPU data matched real processors. For example, /redfish/v1/Systems/System-7/Processors/CPU0 shows this:

"Manufacturer": "Intel(R) Corporation",
"Model": "Multi-Core Intel(R) Xeon(R) processor 7500 Series",
"ProcessorId": {
"VendorId": "GenuineIntel",
"IdentificationRegisters": "0x34AC34DC8901274A",
"EffectiveFamily": "0x42", <<< should be “0x6”
"EffectiveModel": "0x61", <<< should be “0x2E”
"Step": "0x1", <<< should be “0x6”
"MicrocodeInfo": "0x429943" <<< should be “0x0000000B” or higher

If permissible by this team, I'd be happy to make a patch with the correct values. Please let me know. Thanks!

Python dependency files need to be made consistent

There are four files that talk about Python dependencies, but they do not agree with each other:
(1) dependencies/Python Packages to place in directory.txt
(2) install.py
(3) installing_dependencies.txt
(4) requirements.txt

It would be good to have at least the requirements.txt file brought up-to-date with what is currently needed. Here is a list of packages that work for stand-alone operation on Windows (I'm not sure about the list of packages needed for other situations):
setuptools
markupsafe
itsdangerous
flask
aniso8601
pytz
flask_httpauth
requests
flask_restful
StringGenerator==0.2.1
urllib3

The version of StringGenerator is specified in this list because the latest version of StringGenerator (0.3.0) does not install on my Windows system.

If the other files that talk about Python dependencies are also still needed, they should be made to be consistent with an updated requirements.txt file.

Singleton POST command fails to add new resource instances created from templates

Many of the dynamic resources in the emulator are supposed to support emulator-only singleton POST commands that create new resource instances from pre-defined templates. These POST commands currently fail, due to a conflict with the way Infragen was added to the emulator.

Infragen uses the emulator’s internal method calls in dynamic resources instead of the Redfish API, and by itself this might have been OK, but in many cases the merged Infragen code made changes to the methods themselves in dynamic resources. These changes are causing failures for the POST commands that are supposed to create new instances from pre-defined templates.

There is no easy way to fix this problem. If the singleton POST commands are fixed to work as originally intended, Infragen will break. If the emulator is left the way it is, the ability to add new instances from pre-defined templates will remain broken. A reasonable solution would be to fix the emulator to work as originally intended, and to revise Infragen to work via the Redfish API instead of using the emulator’s internal method calls.

curl POST request getting a 404 not found return from emulator.py

Both my IBM CentOS server and my local wsl CentOS image are both getting a 404 return on a POST request. It is happening with both http and https requests. I would think the problem may be in the flask module or maybe requests.

Client side:

(venv) [root@IBM-R912JTS2 emulator]# curl -X POST -k -H 'Content-Type: application/json' https://9.114.207.147:5000/redfish/v1/SessionService/Sessions -d '{"UserName":"Admin"}'
curl: (7) Failed to connect to 9.114.207.147 port 5000: Connection refused
(venv) [root@IBM-R912JTS2 emulator]# clear
(venv) [root@IBM-R912JTS2 emulator]# curl -X POST -k -H 'Content-Type: application/json' https://172.24.225.83:5000/redfish/v1/SessionService/Sessions -d '{"UserName":"Admin"}'
""(venv) [root@IBM-R912JTS2 emulator]# curl -X GET -H 'Content-Type: application/json' https://172.24.225.83:5000/redfish/v1/
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
(venv) [root@IBM-R912JTS2 emulator]# curl -k -X GET -H 'Content-Type: application/json' https://172.24.225.83:5000/redfish/v1/
{
"@odata.context": "/redfish/v1/$metadata#ServiceRoot",
"@odata.type": "#ServiceRoot.1.0.0.ServiceRoot",
"@odata.id": "/redfish/v1/",
....

Server side:

(venv) [root@IBM-R912JTS2 emulator]# python3 emulator.py
/mnt/c/Users/C-VS95897/venv/lib64/python3.6/site-packages/requests/init.py:104: RequestsDependencyWarning: urllib3 (1.26.9) or chardet (5.0.0)/charset_normalizer (2.0.12) doesn't match a supported version!
RequestsDependencyWarning)
INFO:root:Mockup folders
['Redfish']

  • Redfish endpoint at localhost:5000
  • Using static mockup
    INFO:root:Init ResourceDictionary.
    INFO:root:Loading Redfish static resources
    INFO:root:Init ResourceDictionary.
  • Use HTTPS
  • Running in Redfish mode
  • Serving Flask app 'g' (lazy loading)
  • Environment: production
    WARNING: This is a development server. Do not use it in a production deployment.
    Use a production WSGI server instead.
  • Debug mode: off
    WARNING:werkzeug: * Running on all addresses.
    WARNING: This is a development server. Do not use it in a production deployment.
    INFO:werkzeug: * Running on https://172.24.225.83:5000/ (Press CTRL+C to quit)
    INFO:werkzeug:172.24.225.83 - - [05/Jul/2022 10:57:23] "POST /redfish/v1/SessionService/Sessions HTTP/1.1" 404 -
    INFO:werkzeug:172.24.225.83 - - [05/Jul/2022 10:58:40] "GET /redfish/v1/ HTTP/1.1" 200 -

Collection POST commands fail to add new resource instances to non-ComputerSystem resource/subresource collections

It should be possible to add new resources to many Redfish collections by doing a POST command to the collection. This currently works for the emulator's Chassis dynamic resource, but it fails for the rest of the emulator’s current set of dynamic resources.

Note: A similar problem with the ComputerSystem collection was reported separately (see #44 ), where the problems seem to be strongly tied to the way Infragen was implemented. The problem being reported here is for the other (non-ComputerSystem) dynamic resources, since it looks like they can be fixed without impacting the way Infragen currently works.

Clarify how to enable HTTPS

Describe that enabling HTTPS, requires the key and cert files to exist in the execution directly - specifically named server.key and server.cert.

how to change this populate-config.json file based on my requirements?

how to change this populate-config.json file based on my requirements?

Chassis Count -1 Cartridge Count -12 or 24 (Server , storage,NIC,FPGA ,GPU) Each Cartridge node count -2 or 4 (if cartridge is server) SwitchCard count -2 Each SwitchCard Device count -2 (Storage ,NIC ,FPGA,GPU)

The odataType of Power

Hello,
I visite the "Chassis/{ChassisId}/Power" URL and find that the "@odata.type" is "#Power.v1_0_0.PowerMetrics", perhaps it should be "#Power.v1_0_0.Power"
Thank you.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.