Code Monkey home page Code Monkey logo

foreman-yml's Introduction

foreman-yml

PyPi License

Make automated foreman configuration as easy as pie.

This script automatically resolves names so you can link templates, hosts, domains with only using their names. It's not required to know their ids beforehand.

Installation

git clone https://github.com/adfinis-sygroup/foreman-yml --recursive
cd foreman-yml
sudo pip install .

Note CentOS/RHEL

::
sudo yum install gcc python-devel python-pip python-argparse -y

Usage

foreman-yml [import|dump|cleanup] /path/to/config.yaml

Configuration

Root node of YAML is always foreman. You can find an configuration example under config/example.yml

Dump current configuration

foreman-yml supports dumping the whole configuration of a remote foreman instance to stdout. Use foreman-yml dump for this feauture.

For dumping, provide an config file with auth settings:

foreman:
  auth:
    url: "https://foreman.lab.local"
    user: username
    pass: password

Then run foreman-yml like this to dump configuration:

foreman-yml dump /path/to/config.yml > foreman_dump.yml

Import settings into foreman

If no keyword or import is provided to foreman-yml, the script tries to import settings provided by yaml-file.

foreman-yml /path/to/config.yml
foreman-yml dump /path/to/config.yml

The following config sections are supported:

Section auth

auth:
  url: "https://foreman.lab.local"
  user: username
  pass: password
  • url URL of your foreman instance
  • user Username for connecting to the API. User should have administrative rights
  • pass Password for the User

Section setting

setting:
  - name: entries_per_page
    value: 42
  - name:  safemode_render
    value: false

Key/Value pair for global foreman settings

  • name Key
  • value Value

Section architecture

architecture:
  - name: x86_64
  - name: i386
  • name Architecture string (Example: 'x86_64')

Section environment

environment:
  - name: production
  - name: development
  - name: staging
  • name Environment name

Section smart-proxy

smart-proxy:
  - name: smproxy01
    url: "http://localhost:8000/"
  • name Smart proxy name
  • url Smart proxy url

Section domain

domain:
  - name: lab.local
    fullname: lab.local is a test domain
    dns-proxy: smproxy01
    parameters:
      - name:  keyname
        value: keyvalue
  • name Domain name
  • fullname Detailed description
  • dns-proxy DNS proxy for the domain. Maps to smart-proxy.name
  • parameters Extra parameters, key/value pair
  • name Key
  • value Value

Section subnet

subnet:
  - name: lab
    network: 192.168.122.0
    mask: 255.255.255.0
    gateway: 192.168.122.1
    dns-primary: 192.168.122.1
    dns-secondary: 8.8.8.8
    ipam: DHCP
    from: 192.168.122.10
    to: 192.168.122.50
    vlanid:
    domain:
      - name: lab.local
    dhcp-proxy: Smart Proxy
    tftp-proxy: Smart Proxy
    dns-proxy:
    boot-mode: DHCP
    network-type: IPv4
  • name Subnet name
  • network Network address
  • mask Network Netmask
  • gateway Network gateway
  • dns-primary Primary DNS server
  • dns-secondary Secondary DNS server
  • ipam IP Address auto suggestion mode for this subnet, valid values are "DHCP", "Internal DB", "None"
  • from Starting IP Address for IP auto suggestion
  • to Ending IP Address for IP auto suggestion
  • vlanid VLAN ID for this subnet
  • domain Domains in which this subnet is part
  • name Domain name, maps to domain.name
  • dhcp-proxy DHCP Proxy to use within this subnet, maps to smart-proxy.name
  • tftp-proxy TFTP Proxy to use within this subnet, maps to smart-proxy.name
  • dns-proxy DNS Proxy to use within this subnet, maps to smart-proxy.name
  • boot-mode Default boot mode for interfaces assigned to this subnet, valid values are "Static", "DHCP"
  • network-type Type or protocol, IPv4 or IPv6, defaults to IPv4, valid values are "IPv4", "IPv6"

Section model

model:
  - name: libvirt
    info: Virtual Machine
    vendor-class: vmware
    hardware-model: esxi6
  • name Model name
  • info Detailed description
  • vendor-class Hardware vendor
  • hardware-model Hardware model

Section medium

medium:
  - name: Ubuntu Mirror
    path: "http://archive.ubuntu.com/ubuntu"
    os-family: Debian
  • name Model name
  • path The path to the medium, can be a URL or a valid NFS server (exclusive of the architecture)
  • os-family Operating system family, available values: AIX, Altlinux, Archlinux, Coreos, Debian, Freebsd, Gentoo, Junos, NXOS, Redhat, Solaris, Suse, Windows

Section partition-table

partition-table:
  - name: Ubuntu Default
    os-family: Debian
    audit-comment: initial import
    layout: |
            #!ipxe
            <%#
            kind: iPXE
            name: RLC iPXE
            oses:
            - Ubuntu 14.04
            %>
            [...]
    locked: false
  • name Partition table name
  • os-family Operating system family, available values: AIX, Altlinux, Archlinux, Coreos, Debian, Freebsd, Gentoo, Junos, NXOS, Redhat, Solaris, Suse, Windows
  • audit-comment Comment for the audit log
  • layout Partition layout
  • locked Whether or not the template is locked for editing

Section provisioning-template

provisioning-template:
    name: Ubuntu Preseed
    template: |
               <%#
              kind: provision
              name: Ubuntu Preseed
              oses:
              - Debian 8.
              %>
              [...]
    snippet: false
    audit-comment: initial import
    template-kind-id: 3
    template-combination-attribute:
    os:
      - name: Debian 8
    locked: false
  • name Partition table name
  • template The provisioning template itself
  • snippet Set to true if template is a snippet only
  • audit-comment Comment for the audit log
  • template_kind_id Template kind id
  • os
  • name Operating system name, maps to os.name
  • locked Whether or not the template is locked for editing

Section os

os:
  - name: Ubuntu
    major: 14
    minor: 4
    description: Ubuntu 14.04 LTS
    family: Debian
    release-name: trusty
    password-hash: SHA512
    architecture:
      - name: x86_64
    provisioning-template:
      - name: Ubuntu PXE
      - name: Ubuntu Preseed
    medium:
      - name: Ubuntu Mirror
    partition-table:
      - name: Ubuntu Default
    parameters:
      version: "14.04"
      codename: "trusty"
  • name Operating system table name
  • major The provisioning template itself
  • minor Set to true if template is a snippet only
  • description Comment for the audit log
  • family Operating system family, available values: AIX, Altlinux, Archlinux, Coreos, Debian, Freebsd, Gentoo, Junos, NXOS, Redhat, Solaris, Suse, Windows
  • release-name OS release name
  • password-hash Root password hash function to use, one of MD5, SHA256, SHA512, Base64
  • architecture
  • name Architecture name, maps to architecture.name
  • provisioning-template
  • name Provisioning template name, maps to provisioning-template.name
  • medium
  • __ name__ Medium name, maps to medium.name
  • partition-table
  • name Ptable name, maps to partition-table.name
  • parameters
  • __ key__ Additional OS settings in format 'keyname': 'keyvalue'

Section hostgroup

hostgroup:
  - name: switzerland
    parent:
    environment: production
    os: Ubuntu 14.04 LTS
    architecture: x86_64
    medium: Ubuntu Mirror
    partition-table: Ubuntu Default
    subnet: lab
    domain: lab.local
    parameters:
      - keyname:  keyvalue
  • name Hostgroup name
  • parent Parent hostgroup
  • environment Environment name, maps to environment.name
  • os Operating system name, maps to os.name
  • architecture Architecture name, maps to architecture.name
  • medium Media name, maps to medium.name
  • partition-table Ptable name, maps to partition-table.name
  • subnet Subnet name, maps to subnet.name
  • domain Domain name, maps to domain.name
  • parameters Dict of params -keyname Value of param

Section host

host:
  - name: testhost
    domain: lab.local
    architecture: x86_64
    hostgroup: switzerland
    environment: production
    os: Ubuntu 14.04 LTS
    media: Ubuntu Mirror
    partition: Ubuntu Default
    model: VMWare VM
    mac: 00:11:22:33:44:55
    root-pass: supersecret42
    parameters:
      env: prod
      kernel_params: quiet
  • name Host name
  • domain Domain name, maps to domain.name
  • architecture Architecture name, maps to architecture.name
  • hostgroup Hostgroup name, maps to hostgroup.name
  • environment Environment name, maps to environment.name
  • os Operating system name, maps to os.name
  • media Media name, maps to medium.name
  • partition Ptable name, maps to partition.name
  • model Hardware model name, maps to model.name
  • mac MAC address
  • root-pass Root password
  • parameters Dict of params
  • keyname Value of param

Section roles

roles:
  - name: testrole
    permissions:
      architecture:
        - view_architectures
        - edit_architectures
      compute_resources:
        - view_compute_resources
        - create_compute_resources
        - destroy_compute_resources
  • name Role name
  • permissions
  • groupname Name of permission group (not applied to foreman), only for clarity
    • permission_name Permission name, maps to permission.name
    • permission_name Permission name, maps to permission.name
    • permission_name Permission name, maps to permission.name
    • ... ...

Section users

users:
  - login: testhaaaans
    password: schmetterling42
    mail: [email protected]
    auth-source: ldap-is-not-web-scale
    firstname: Test
    lastname: Haaaaaans
    admin: true
    timezone: UTC
    locale: en
  • login User login
  • password Password of user
  • auth-source Name of auth source or 'INTERNAL' for foreman-own auth source
  • firstname First name of user
  • lastname Last name of user
  • admin If true, user will be created with admin permissions
  • timezone Timezone for the user
  • locale WebUI locale for the user

Section usergroups

usergroups:
  - name: api-test2
    admin: false
    users:
      - name: foo
      - name: burlson
    groups:
      - name: api-testgroup
    ext-usergroups:
      - name: foremangroup
        auth-source-ldap: ldap-is-not-web-scale
    roles:
      - name: foo
  • name Usergroup name
  • admin If set to true or 1, group is has admin permissions
  • users List of users
  • name Username, maps to users.name
  • groups List of groups
  • name Groupname, maps to usergroups.name
  • ext-usergroups List of external usergroups
  • name Name of the external usergroup
  • auth-source-ldap Name of the external auth source, maps to auth-source-ldap.name
  • roles List of roles
  • name Role name, maps to role.name

Section auth-source-ldap

auth-source-ldap:
  - name: ldap-is-not-web-scale
    host: 10.11.12.13
    port: 389
    account: uid=binduser,cn=users,dc=test,dc=example,dc=com
    account-password: 123qwe
    base-dn: dc=test,dc=example,dc=com
    attr-login: uid
    attr-firstname: firstName
    attr-lastname: lastName
    attr-mail: mail
    attr-photo: picture
    onthefly-register: false
    usergroup-sync: false
    tls: false
    groups-base: cn=groups,dc=test,dc=example,dc=com
    ldap-filter:
    server-type: posix
  • name Name of the authsource
  • host LDAP host
  • port Server port
  • account Bind account user
  • account-password Bind account password
  • base-dn LDAP Base DN
  • attr-login LDAP attribute for username, required if onthefly-register is true
  • attr-firstname LDAP attribute for first name, required if onthefly-register is true
  • attr-lastname LDAP attribute for last name, required if onthefly-register is true
  • attr-mail LDAP attribute for mail, required if onthefly-register is true
  • attr-photo LDAP attribute for user photo
  • onthefly-register Register users on the fly if true or 1
  • usergroup-sync Sync external user groups on login if true or 1
  • tls If true or 1, use SSL to connect to the server
  • groups-base groups base DN
  • ldap-filter LDAP filter
  • server-type LDAP Server type, valid are free_ipa, active_directory and posix

Cleanup (delete) settings

If the keyword cleanup is provided to foreman-yml, it will try to delete items specified by its name.

foreman-yml cleanup /path/to/config.yml

Section cleanup-[architecture|compute-profile|partition-table|provisioning-template]

cleanup-[architecture|compute-profile|partition-table|provisioning-template]:
  - name: foo
  - name: bar

Removes specified objects, mapping to object.name - name architecture|compute-profile|partition-table|provisioning-template name to delete

Hacking

virtualenv --system-site-packages venv-dev
source venv-dev/bin/activate
pip install -e .

Docker

docker build -t foreman-yml .
docker run foreman-yml dump my-server-config.yml > my-server.dump
# specific snowflake configuration
docker run -ti -v $(pwd)/configs:/foreman-yml/configs foreman-yml dump configs/snowflake.yml

Future

  • Dump current settings
  • Better documentaion

License

GNU GENERAL PUBLIC LICENSE Version 3

foreman-yml's People

Contributors

alexjfisher avatar andreabettich avatar eni avatar eni23 avatar erickellerek1 avatar karras avatar keachi avatar tongpu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

foreman-yml's Issues

Running fails with NameError: global name 'MultipleInvalid' is not defined

When using foreman-yml from foreman-ansible my runs fail with the following traceback:

Traceback (most recent call last):
  File "/usr/local/bin/foreman-yml", line 9, in 
    load_entry_point('foreman-yaml==0.0.2', 'console_scripts', 'foreman-yml')()
  File "/usr/local/lib/python2.7/dist-packages/foreman_yaml-0.0.2-py2.7.egg/foremanclient/foreman_yml.py", line 142, in main
    fm_import(fm_imp)
  File "/usr/local/lib/python2.7/dist-packages/foreman_yaml-0.0.2-py2.7.egg/foremanclient/foreman_yml.py", line 64, in fm_import
    fm.process_config_provisioningtpl()
  File "/usr/local/lib/python2.7/dist-packages/foreman_yaml-0.0.2-py2.7.egg/foremanclient/importer.py", line 330, in process_config_provisioningtpl
    except MultipleInvalid as e:
NameError: global name 'MultipleInvalid' is not defined

After adding from voluptuous import MultipleInvalid at the top of foreman-yml/foremanclient/importer.py the run succeeds.

Subnet domain export vs import

Exporting subnets has the following structure:

subnet:
- name: xyz
  otherkeys: othervalues
  domain:
  - mydomain.example.org

The importer however expects this format:

subnet:
- name: xyz
  otherkeys: othervalues
  domain:
  - name: mydomain.example.org

Location and organization

Is there a specific reason why locations and organizations are not part of the plugin?

If not, do you mind me adding the required functions within the code for configuring location and organizations? Additionaly i'd like to add the configuration of both to each object that can accept them.

Add documentation for log level

There are a number of configuration options that don't work during import and it is not possible to do a dump from one instance and use the yaml produced unmodified to populate a new instance. I hope to work on these problems (and ultimately submit a PR) but I don't see a simple way to increase the logging level to DEBUG. Will you please add this to the documentation?

Provisioning template configuration without OS association

Currently it's not possible to deploy provisioning templates without any OS association. Certain templates shouldn't be associated with any OS.

[INFO] Create Provisioning Template 'Custom PXEGrub2 global default'
Traceback (most recent call last):
  File "/usr/local/bin/foreman-yml", line 9, in <module>
    load_entry_point('foreman-yml==1.0.4', 'console_scripts', 'foreman-yml')()
  File "build/bdist.linux-x86_64/egg/foreman_yml/main.py", line 137, in main
  File "build/bdist.linux-x86_64/egg/foreman_yml/main.py", line 63, in fm_import
  File "build/bdist.linux-x86_64/egg/foreman_yml/importer.py", line 350, in process_config_provisioningtpl
TypeError: 'NoneType' object is not iterable

Tested configuration:

  provisioning-template:
    - name: Custom PXEGrub2 global default
      template: |
                <%#
                kind: PXEGrub2
                name: Custom PXEGrub2 global default
                model: ProvisioningTemplate
                %>
                DEFAULT menu
                PROMPT 0
                MENU TITLE PXE Menu
                TIMEOUT 100
                TOTALTIMEOUT 6000
                ONTIMEOUT local

                LABEL local
                MENU LABEL boot from local disk
                MENU DEFAULT
                LOCALBOOT 0
      snippet: false
      audit-comment: initial import
      template-kind-id: 10
      template-combination-attribute:
        - hostgroup:
          environment:
      os:
      locked: true

Fix log message for user creation error

The variables username and auth source need to be exchanged in the following log message:

[ERROR] Cannot resolve auth source 'cool_user' for user 'INTERNAL', skipping creation

Empty operating system parameters

If one defines no key/values for the operating system parameters foreman-yml fails because it tries to loop over an non initialized variable.

Install fails on CentOS 7.3.1611

The install instructions are short & sweet, but I can't get them to work on a clean CentOS 7.3.1611 system. All updates have been applied and python2-pip-8.1.2-5.el7.noarch is installed to provide pip.

The result I get is:

[root@katello foreman-yml]# pip install .
Processing /root/foreman-yml
Collecting pyyaml (from foreman-yml==1.0.2)
Using cached PyYAML-3.12.tar.gz
Requirement already satisfied (use --upgrade to upgrade): requests in /usr/lib/python2.7/site-packages (from foreman-yml==1.0.2)
Collecting python-foreman (from foreman-yml==1.0.2)
Using cached python-foreman-0.4.14.tar.gz
Complete output from command python setup.py egg_info:
zip_safe flag not set; analyzing archive contents...

Installed /tmp/pip-build-j7A5Zt/python-foreman/autosemver-0.2.4_-py2.7.egg
Searching for dulwich
Reading https://pypi.python.org/simple/dulwich/
Best match: dulwich 0.16.3
Downloading https://pypi.python.org/packages/9e/d7/8fb5b952ad14f27f7ab1bbe17db7860fe99c3c3e5d08de0bea3a161389a0/dulwich-0.16.3.tar.gz#md5=632280ffa88f732ba61ed320f7bb60aa
Processing dulwich-0.16.3.tar.gz
Writing /tmp/easy_install-YQ1swJ/dulwich-0.16.3/setup.cfg
Running dulwich-0.16.3/setup.py -q bdist_egg --dist-dir /tmp/easy_install-YQ1swJ/dulwich-0.16.3/egg-dist-tmp-6KXWKV
warning: no files found matching 'README.swift'
warning: no files found matching 'HACKING'
warning: no files found matching 'CONTRIBUTING'
warning: no files found matching 'relicensing-apachev2.txt'
unable to execute gcc: No such file or directory
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/tmp/pip-build-j7A5Zt/python-foreman/setup.py", line 35, in <module>
    'Programming Language :: Python :: 3.4',
  File "/usr/lib64/python2.7/distutils/core.py", line 112, in setup
    _setup_distribution = dist = klass(attrs)
  File "/usr/lib/python2.7/site-packages/setuptools/dist.py", line 265, in __init__
    self.fetch_build_eggs(attrs.pop('setup_requires'))
  File "/usr/lib/python2.7/site-packages/setuptools/dist.py", line 289, in fetch_build_eggs
    parse_requirements(requires), installer=self.fetch_build_egg
  File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 618, in resolve
    dist = best[req.key] = env.best_match(req, self, installer)
  File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 862, in best_match
    return self.obtain(req, installer) # try and download/install
  File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 874, in obtain
    return installer(requirement)
  File "/usr/lib/python2.7/site-packages/setuptools/dist.py", line 339, in fetch_build_egg
    return cmd.easy_install(req)
  File "/usr/lib/python2.7/site-packages/setuptools/command/easy_install.py", line 623, in easy_install
    return self.install_item(spec, dist.location, tmpdir, deps)
  File "/usr/lib/python2.7/site-packages/setuptools/command/easy_install.py", line 653, in install_item
    dists = self.install_eggs(spec, download, tmpdir)
  File "/usr/lib/python2.7/site-packages/setuptools/command/easy_install.py", line 849, in install_eggs
    return self.build_and_install(setup_script, setup_base)
  File "/usr/lib/python2.7/site-packages/setuptools/command/easy_install.py", line 1130, in build_and_install
    self.run_setup(setup_script, setup_base, args)
  File "/usr/lib/python2.7/site-packages/setuptools/command/easy_install.py", line 1118, in run_setup
    raise DistutilsError("Setup script exited with %s" % (v.args[0],))
distutils.errors.DistutilsError: Setup script exited with error: command 'gcc' failed with exit status 1

----------------------------------------

Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-j7A5Zt/python-foreman/
You are using pip version 8.1.2, however version 9.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
[root@katello foreman-yml]#

I have tried doing a pip install --upgrade pip, but this just results in a different error and a pip installation which I can't fix or amend as the pip upgrade has overwritten the RPM which the RPM database still thinks is valid.

Host creation failure

Hey,

I'm adding some extra functionalities including the option to specify location and organizations. Basically the script will ask if organization and/or locations are used and than enable these options. I'm running into a small problem when creating hosts though i've traced it to the following part of code and I'm wondering what it specifically does:

 try:
                    fixif = {
                        'id':           fmh['interfaces'][0]['id'],
                        'managed':      'false',
                        'primary':      'true',
                        'provision':    'true'
                    }
                    fixhost = {
                        'interfaces_attributes':        [ fixif ],
                        'host_parameters_attributes':   host_params
                    }
                    try:
                        self.fm.hosts.update(fixhost, fmh['id'])
                        return fmh
                    except:
                        log.log(log.LOG_DEBUG, "An Error Occured when linking Host '{0}' (non-fatal)".format(hostc['name']))
                except:
pass

I've removed it in my test setup and I can't detect any changes, I'm assuming this was added to fix some kind of error that occured but I don't have any errors when I run foreman-yaml without it.

Document host configuration

The documentation currently lacks an example for a host configuration. Please add it to the example config and README.

Support cleanup of locked templates

Currently it's not possible to cleanup the default provisioning templates because they're locked:

--------- data received ------------
{
  "error": {"id":10,"errors":{"base":["This template is locked and may not be removed."]},"full_messages":["This template is locked and may not be removed."]}
}

Add Travis CI

Install/Build with Travis CI and deploy it to PyPi.

Issues with dumping subnets

When dumping subnets, all domains goes to domains instead of domain. This makes the produced dump unusable for importing with foreman-yml.

Also voluptuous.MultipleInvalid is not imported correctly in importer.py and cleanup.py

Thanks @mhalder for reporting those issues.

Implement dumping feature

It would be great to have some kind of dumping feature to export the configuration of a Foreman instance into a YAML file.

Add json definition for 1.13

I believe in order to use the 1.13 Foreman API we need to add the json definition. I would have pushed a PR but i don't know how to generate the json file.

Allow disabling SSL verification warnings

Currently foreman-yml displays SSL warnings when connecting via HTTPS:

/usr/lib/python2.7/site-packages/urllib3-1.23-py2.7.egg/urllib3/connectionpool.py:857:
InsecureRequestWarning: Unverified HTTPS request is being made.
Adding certificate verification is strongly advised. 
See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings

There should be a CLI parameter or similar to disable those warnings.

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.