Giter Club home page Giter Club logo

finsy's Introduction

Finsy P4Runtime Controller Library

pypi documentation ci codecov codespace

Finsy is a P4Runtime controller library written in Python using asyncio. Finsy includes support for gNMI.

Check out the examples directory for some demonstration programs.

Installation

Finsy requires Python 3.10 or later. To install the latest version, type pip install finsy.

P4Runtime Scripts

With Finsy, you can write a Python script that reads/writes P4Runtime entities for a single switch.

Here is a complete example that retrieves the P4Info from a switch:

import finsy as fy

async def main():
    async with fy.Switch("sw1", "127.0.0.1:50001") as sw1:
        # Print out a description of the switch's P4Info, if one is configured.
        print(sw1.p4info)

fy.run(main())

Here is another example that prints out all non-default table entries.

import finsy as fy

async def main():
    async with fy.Switch("sw1", "127.0.0.1:50001") as sw1:
        # Do a wildcard read for table entries.
        async for entry in sw1.read(fy.P4TableEntry()):
            print(entry)

fy.run(main())

P4Runtime Controller

You can also write a P4Runtime controller that manages multiple switches independently. Your controller can react to events from the Switch by changing the contents of P4 tables.

Each switch is managed by an async ready_handler function. Your ready_handler function can read or update various P4Runtime entities in the switch. It can also create tasks to listen for packets or digests.

When you write P4Runtime updates to the switch, you use a unary operator (+, -, ~) to specify the operation: INSERT (+), DELETE (-) or MODIFY (~).

import finsy as fy

async def ready_handler(sw: fy.Switch):
    await sw.delete_all()
    await sw.write(
        [
            # Insert (+) multicast group with ports 1, 2, 3 and CONTROLLER.
            +fy.P4MulticastGroupEntry(1, replicas=[1, 2, 3, 255]),
            # Modify (~) default table entry to flood all unmatched packets.
            ~fy.P4TableEntry(
                "ipv4",
                action=fy.Action("flood"),
                is_default_action=True,
            ),
        ]
    )

    async for packet in sw.read_packets():
        print(f"{sw.name}: {packet}")

Use the SwitchOptions class to specify each switch's settings, including the p4info/p4blob and ready_handler. Use the Controller class to drive multiple switch connections. Each switch will call back into your ready_handler function after the P4Runtime connection is established.

from pathlib import Path

options = fy.SwitchOptions(
    p4info=Path("hello.p4info.txt"),
    p4blob=Path("hello.json"),
    ready_handler=ready_handler,
)

controller = fy.Controller([
    fy.Switch("sw1", "127.0.0.1:50001", options),
    fy.Switch("sw2", "127.0.0.1:50002", options),
    fy.Switch("sw3", "127.0.0.1:50003", options),
])

fy.run(controller.run())

Your ready_handler can spawn concurrent tasks with the Switch.create_task method. Tasks created this way will have their lifetimes managed by the switch object.

If the switch disconnects or its role changes to backup, the task running your ready_handler (and any tasks it spawned) will be cancelled and the ready_handler will begin again.

For more examples, see the examples directory.

Switch Read/Write API

The Switch class provides the API for interacting with P4Runtime switches. You will control a Switch object with a "ready handler" function. The ready handler is an async function that is called when the switch is ready to accept commands.

Your ready handler will typically write some control entities to the switch, then listen for incoming events and react to them with more writes. You may occasionally read entities from the switch.

When your ready handler is invoked, there is already a P4Runtime channel established, with client arbitration completed, and pipeline configured as specified in SwitchOptions.

Here is an example skeleton program. The ready handler is named ready().

async def ready(switch: fy.Switch):
    # Check if switch is the primary. If not, we may want to proceed
    # in read-only mode. In this example, ignore switch if it's a backup.
    if not switch.is_primary:
        return

    # If we're reconnecting to a switch, it will already have runtime state.
    # In this example, we just delete all entities and start over.
    await switch.delete_all()

    # Provision the pipeline with one or more `write` transactions. Each
    # `write` is a single WriteRequest which may contain multiple updates.
    await switch.write(
        # [Next section will cover what goes here.]
    )

    # Listen for events and respond to them. This "infinite" loop will
    # continue until the Switch disconnects, changes primary/backup status,
    # or the controller is stopped.
    async for packet in switch.read_packets():
        await handle_packet(switch, packet)

The Switch class provides a switch.create_task method to start a managed task. Tasks allow you to perform concurrent operations on the same switch. We could have written the last stanza above that reads packets in an infinite loop as a separate task. It's okay for the ready handler function to return early; any tasks it created will still run.

Writes

Use the write() method to write one or more P4Runtime updates and packets.

A P4Runtime update supports one of three operations: INSERT, MODIFY or DELETE. Some entities support all three operations. Other entities only support MODIFY.

Entity Operations Permitted Related Classes
P4TableEntry INSERT, MODIFY, DELETE Match, Action, IndirectAction, P4MeterConfig, P4CounterData, P4MeterCounterData
P4ActionProfileMember INSERT, MODIFY, DELETE
P4ActionProfileGroup INSERT, MODIFY, DELETE P4Member
P4MulticastGroupEntry INSERT, MODIFY, DELETE
P4CloneSessionEntry INSERT, MODIFY, DELETE
P4DigestEntry INSERT, MODIFY, DELETE
P4ExternEntry INSERT, MODIFY, DELETE
P4RegisterEntry MODIFY
P4CounterEntry MODIFY P4CounterData
P4DirectCounterEntry MODIFY P4CounterData
P4MeterEntry MODIFY P4MeterConfig, P4MeterCounterData
P4DirectMeterEntry MODIFY
P4ValueSetEntry MODIFY P4ValueSetMember

Insert/Modify/Delete Updates

To specify the operation, use a unary + (INSERT), ~ (MODIFY), or - (DELETE). If you do not specify the operation, write will raise a ValueError exception.

Here is an example showing how to insert and delete two different entities in the same WriteRequest.

await switch.write([
    +fy.P4TableEntry(          # unary + means INSERT
        "ipv4", 
        match=fy.Match(dest="192.168.1.0/24"),
        action=fy.Action("forward", port=1),
    ),
    -fy.P4TableEntry(          # unary - means DELETE
        "ipv4", 
        match=fy.Match(dest="192.168.2.0/24"),
        action=fy.Action("forward", port=2),
    ),
])

You should not insert, modify or delete the same entry in the same WriteRequest.

If you are performing the same operation on all entities, you can use the Switch insert, delete, or modify methods.

await switch.insert([
    fy.P4MulticastGroupEntry(1, replicas=[1, 2, 3]),
    fy.P4MulticastGroupEntry(2, replicas=[4, 5, 6]),
])

Modify-Only Updates

For entities that only support the modify operation, you do not need to specify the operation. (You can optionally use ~.)

await switch.write([
    fy.P4RegisterEntry("reg1", index=0, data=0),
    fy.P4RegisterEntry("reg1", index=1, data=1),
    fy.P4RegisterEntry("reg1", index=2, data=2),
])

You can also use the modify method:

await switch.modify([
    fy.P4RegisterEntry("reg1", index=0, data=0),
    fy.P4RegisterEntry("reg1", index=1, data=1),
    fy.P4RegisterEntry("reg1", index=2, data=2),
])

If you pass a modify-only entity to the insert or delete methods, the P4Runtime server will return an error.

Sending Packets

Use the write method to send a packet.

await switch.write([fy.P4PacketOut(b"a payload.....", port=3)])

You can include other entities in the same call. Any non-update objects (e.g. P4PacketOut, P4DigestListAck) will be sent before the WriteRequest.

Listening for Packets

To receive packets, use the async iterator Switch.read_packets(). In this example, pkt is a P4PacketIn object.

read_packets can filter for a specific eth_type.

# Read packets filtering only for ARP (eth_type == 0x0806).
async for pkt in switch.read_packets(eth_types={0x0806}):
    # You can access the packet payload `pkt.payload` or any metadata value,
    # e.g. `pkt['ingress_port']`
    print(pkt.payload)
    print(pkt['ingress_port'])

Listening for Digests

To receive digests, use the async iterator Switch.read_digests. You must specify the name of the digest from your P4 program.

async for digest in switch.read_digests("digest_t"):
    # You can access the digest metadata e.g. `digest['ingress_port']`
    # Your code may need to update table entries based on the digest data.
    # To ack the digest, write `digest.ack()`.
    await switch.write([entry, ...])
    await switch.write([digest.ack()])

To acknowledge the digest entry, you can write digest.ack().

Listening for Idle Timeouts

To receive idle timeout notifications, use the async iterator Switch.read_idle_timeouts. You will receive a P4IdleTimeoutNotification which contains multiple table entries -- one for each entry that timed out.

async for timeout in switch.read_idle_timeouts():
    for entry in timeout.table_entry:
        print(timeout.timestamp, entry)

Other Events

A P4 switch may report other events using the EventEmitter API. See the SwitchEvent class for the event types. Each switch has a switch.ee attribute that lets your code register for event callbacks.

Development and Testing

Perform these steps to set up your local environment for Finsy development, or try the codespace. Finsy requires Python 3.10 or later. If poetry is not installed, follow these directions to install it.

Clone and Prepare a Virtual Environment

The poetry install command installs all development dependencies into the virtual environment (venv).

$ git clone https://github.com/byllyfish/finsy.git
$ cd finsy
$ python3 -m venv .venv
$ poetry install

Run Unit Tests

When you run pytest from the top level of the repository, you will run the unit tests.

$ poetry run pytest

Run Integration Tests

When you run pytest from within the examples directory, you will run the integration tests instead of the unit tests. The integration tests run the example programs against a Mininet network. Docker or podman are required.

$ cd examples
$ poetry run pytest

finsy's People

Contributors

byllyfish avatar dependabot[bot] avatar step-security-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

finsy's Issues

delete_all: Deleting const entries from a table (error on BMV2)

The tb_int_inst_0407 table has is_const_table: true.

The Switch.delete_all() function needs to skip over these const entries.

  [details.31] P4Error(canonical_code=GRPCStatusCode.NOT_FOUND, message='Cannot find match entry', space='ALL-sswitch-p4org', code=0, subvalue=type: DELETE
      entity {
        table_entry {
          table_id: 46152431  # egress.Int_transit.tb_int_inst_0407
          match {
            field_id: 1  # hdr.int_header.instruction_mask
            ternary {
              value: "\017\000"  # 0f00
              mask: "\017\000"  # 0f00
            }
          }
          action {
            action {
              action_id: 23726833  # egress.Int_transit.int_set_header_0407_i15
            }
          }
          priority: 2147483631
        }
      })

Difference in cancellation behavior between Controller.run() and async context manager

When the task running Controller.run() is cancelled, the CancelledError is swallowed up by run(). That is, run() shuts down the controller cleanly, but does not re-raise the CancelledError.

When the task is running in the Controller context manager, the controller shuts down cleanly, and then re-raises the CancelledError. I think this is the correct behavior.

This means that example code using run() will need to catch both KeyboardInterrupt and asyncio.CancelledError.

Alias P4TableMatch, P4TableAction.

It may make sense to alias some of the helper classes that constitute a TableEntry like P4TableMatch and P4TableAction.

fy.match = P4TableMatch
fy.action = P4TableAction

This can lead to simpler coding of table entries:

entry = fy.P4TableEntry(
    table_id="x",
    match=fy.match(x=1),
    action=fy.action("forward", x=1),
)

P4TableAction can be extended to support __call__ syntax. This will allow you to pre-define your actions then just use python syntax to specify the params.

forward = fy.action("forward")

entry = fy.P4TableEntry(
    table_id="x",
    action=forward(dest="1.2.3.4"),
    is_default_action=True,
)

Reconsider P4TableEntry's match_str() and action_str() methods use of P4Schema.current().

Consider making P4TableEntry.match_str() and P4TableEntry.action_str() easier to use without any p4info setting.

If you pass p4info, you will get p4info-aware formatting.
If you don't pass p4info, you will get the default formatting.

Using the wildcard option will require passing a p4info value.

Consider removing the P4Schema context manager API; other than the match_dict(), match_str() and action_str() API's , it is also used in P4ActionProfileMember.action_str()... That API should behave similar to P4TableEnty.action_str().

Python 3.11 segmentation fault in grpc test; seg. fault in task.get_stack(limit=1) accessing frame.f_back

Using grpcio 1.50.0 and Python 3.11.0, the test test_switch1 will fail with a segmentation fault.

Current thread 0x000000011678b600 (most recent call first):
  File "/usr/local/Cellar/[email protected]/3.11.0/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/base_tasks.py", line 53 in _task_get_stack
  File "/Users/bfish/code/finsy/.venv_311/lib/python3.11/site-packages/grpc/aio/_channel.py", line 315 in _close
  File "/Users/bfish/code/finsy/.venv_311/lib/python3.11/site-packages/grpc/aio/_channel.py", line 374 in close
  File "/Users/bfish/code/finsy/finsy/p4client.py", line 308 in close
  File "/Users/bfish/code/finsy/finsy/switch.py", line 393 in _run

If I disable the test P4RuntimeServer with an environment variable, the test succeeds.

It seems like there is a memory misbehavior bug in the grpc async server implementation, but client code is tripping over it in _task_get_stack. If I run client code without running any of the server test code, there is no segmentation fault.

For now, the CI tests for Python 3.11 are running with FINSY_TEST_P4RUNTIME_SERVER=skip and FINSY_TEST_GNMI_SERVER=skip.

GNMIPath: Minor issues in parse/to_str.

  1. There should be an error on parse if the same key appears more than once: "x[a=1][a=2]".
  2. to_str needs to escape white spaces.
  3. GNMIPath should support copy constructor from an existing GNMIPath.
>>> from finsy import GNMIPath
>>> p = GNMIPath("\\x20")
>>> p
GNMIPath(' ')
>>> GNMIPath(p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/bfish/code/finsy/finsy/gnmipath.py", line 178, in __repr__
    path = gnmistring.to_str(self.path)
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 124, in to_str
    if not path.elem:
AttributeError: 'GNMIPath' object has no attribute 'elem'
>>> GNMIPath(str(p))
Traceback (most recent call last):
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 110, in parse
    elems = _elems.parse(value)
  File "/Users/bfish/code/finsy/.venv/lib/python3.10/site-packages/parsy/__init__.py", line 87, in parse
    (result, _) = (self << eof).parse_partial(stream)
  File "/Users/bfish/code/finsy/.venv/lib/python3.10/site-packages/parsy/__init__.py", line 101, in parse_partial
    raise ParseError(result.expected, stream, result.furthest)
parsy.ParseError: expected one of '/', 'EOF', '_IDENT' at 0:0

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/bfish/code/finsy/finsy/gnmipath.py", line 71, in __init__
    path = gnmistring.parse(path)
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 113, in parse
    raise ValueError(f"parse failed: {ex} (value={value!r})") from ex
ValueError: parse failed: expected one of '/', 'EOF', '_IDENT' at 0:0 (value=' ')
>>> GNMIPath('\xa0')
Traceback (most recent call last):
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 110, in parse
    elems = _elems.parse(value)
  File "/Users/bfish/code/finsy/.venv/lib/python3.10/site-packages/parsy/__init__.py", line 87, in parse
    (result, _) = (self << eof).parse_partial(stream)
  File "/Users/bfish/code/finsy/.venv/lib/python3.10/site-packages/parsy/__init__.py", line 101, in parse_partial
    raise ParseError(result.expected, stream, result.furthest)
parsy.ParseError: expected one of '/', 'EOF', '_IDENT' at 0:0

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/bfish/code/finsy/finsy/gnmipath.py", line 71, in __init__
    path = gnmistring.parse(path)
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 113, in parse
    raise ValueError(f"parse failed: {ex} (value={value!r})") from ex
ValueError: parse failed: expected one of '/', 'EOF', '_IDENT' at 0:0 (value='\xa0')
>>> GNMIPath('\\xa0')
GNMIPath('\\u00a0')
>>> str(_)
'\\u00a0'
>>> GNMIPath("x[a=1][a=2]")
GNMIPath('x[a=2]')
>>> GNMIPath("\\xa1")
GNMIPath('\\u00a1')
>>> GNMIPath("•")
GNMIPath('\\u2022')
>>> GNMIPath("\u2022")
GNMIPath('\\u2022')
>>> GNMIPath("\xa0")
Traceback (most recent call last):
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 110, in parse
    elems = _elems.parse(value)
  File "/Users/bfish/code/finsy/.venv/lib/python3.10/site-packages/parsy/__init__.py", line 87, in parse
    (result, _) = (self << eof).parse_partial(stream)
  File "/Users/bfish/code/finsy/.venv/lib/python3.10/site-packages/parsy/__init__.py", line 101, in parse_partial
    raise ParseError(result.expected, stream, result.furthest)
parsy.ParseError: expected one of '/', 'EOF', '_IDENT' at 0:0

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/bfish/code/finsy/finsy/gnmipath.py", line 71, in __init__
    path = gnmistring.parse(path)
  File "/Users/bfish/code/finsy/finsy/gnmistring.py", line 113, in parse
    raise ValueError(f"parse failed: {ex} (value={value!r})") from ex
ValueError: parse failed: expected one of '/', 'EOF', '_IDENT' at 0:0 (value='\xa0')
>>> GNMIPath("\xa1")
GNMIPath('\\u00a1')
>>> 

Wildcard reads using an action_id needs work...

The P4Runtime spec mentions that you can do wildcard reads based on the action_id. Finsy is currently too strict, and complains if you don't provide all the parameters.

>>> p4info = P4Schema(Path('examples/ngsdn/ngsdn/p4/main.p4info.txt'))
>>> entry = P4TableEntry('srv6_transit', action=P4TableAction('srv6_t_insert_2'))
>>> entry.encode(p4info)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/bfish/code/finsy/finsy/p4entity.py", line 629, in encode
    return p4r.Entity(table_entry=self.encode_entry(schema))
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bfish/code/finsy/finsy/p4entity.py", line 645, in encode_entry
    action = self.action.encode_table_action(table)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bfish/code/finsy/finsy/p4entity.py", line 340, in encode_table_action
    action_p4 = self._encode_action(action)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/bfish/code/finsy/finsy/p4entity.py", line 384, in _encode_action
    self._fail_missing_params(action)
  File "/Users/bfish/code/finsy/finsy/p4entity.py", line 362, in _fail_missing_params
    raise ValueError(f"Action {action.alias!r}: missing parameters {seen}")
ValueError: Action 'srv6_t_insert_2': missing parameters {'s2', 's1'}

If you provide a P4TableAction with no arguments, it should be accepted for read requests.

When encoding updates, Finsy should require all parameters.

finsy and the optional shellous dependency

Running the example programs with demonet requires shellous, but it is not a base dependency. Add support for installing extra dependencies like shellous to run the demonet examples.

This should be done using extra dependencies, i.e. pip install finsy[demonet]

macos: GRPC assertion failures and hangs occasionally show up in CI builds

  1. GRPC assertion in tests. (grpcio-1.58.0, protobuf-4.24.3, py-3.10/macos-latest) (Action 826)
E0928 20:25:17.313613000 4560561664 sync.cc:57]                        ASSERTION FAILED: pthread_mutex_lock(mu) == 0
Fatal Python error: Aborted
  1. When running code coverage, a "Waiting" message that persists until timeout. (grpcio-1.58.0, protobuf-4.24.3, py-3.11/macos-latest) (Action 818)
E0917 20:51:29.169858000 4742030848 original_thread_pool.cc:232]       Waiting for thread pool to idle before forking (1 to 0)
E0917 20:51:32.298273000 4742030848 original_thread_pool.cc:232]       Waiting for thread pool to idle before forking (1 to 0)
E0917 20:51:35.325750000 4742030848 original_thread_pool.cc:232]       Waiting for thread pool to idle before forking (1 to 0)
E0917 20:51:38.401483000 4742030848 original_thread_pool.cc:232]       Waiting for thread pool to idle before forking (1 to 0)
... (continues until github action times out)
  1. GRPC assertion in tests. (grpcio-1.57.0, protobuf-4.24.2, py-3.11/macos-latest) (Action 810)
E0911 01:50:00.267984000 4360189440 sync.cc:57]                        ASSERTION FAILED: pthread_mutex_lock(mu) == 0
Fatal Python error: Aborted

Change use of "pipeline" in logging with "p4program".

Logging uses the word "Pipeline" -- change this to "P4 Program"

Instead of:

1713225911.362 INFO finsy [sw1] Channel up (is_primary=True, role_name='', p4r=1.3.0): 
1713225911.376 INFO finsy [sw1] Pipeline installed: pipeline='prog.p4' version='0.1' arch='v1model'
1713225911.376 INFO finsy [sw1] Channel ready (is_primary=True, role_name=''): pipeline='prog.p4' version='0.1' arch='v1model'

Do this instead:

1713225911.362 INFO finsy [sw1] Channel up (is_primary=True, role_name='', p4r=1.3.0): 
1713225911.376 INFO finsy [sw1] P4 Program installed: p4program='prog.p4' version='0.1' arch='v1model'
1713225911.376 INFO finsy [sw1] Channel ready (is_primary=True, role_name=''): p4program='prog.p4' version='0.1' arch='v1model'

CI testing: GNMIServer shutting down shows "asyncio Task was destroyed but it is pending!" message

Appears to be related to GNMI subscribe.

1713546777.879 DEBUG grpc._cython.cygrpc RPC cancelled for servicer method [/gnmi.gNMI/Subscribe]
1713546777.879 DEBUG finsy [Task-100] GNMIClient: close channel '127.0.0.1:49694'
1713546777.890 DEBUG finsy [Task-101] GNMIServer: server stopped: 127.0.0.1:49694
1713546777.910 ERROR asyncio Task was destroyed but it is pending!
task: <Task pending name='Task-102' coro=<<async_generator_athrow without __name__>()>>

AssertionError: Run hello/net/run.py to report an error

When I try to execute run.py, it returns the following error:

pth@ubuntu:~/Workspace/finsy_test/finsy-master/examples/hello$ ./net/run.py 
Traceback (most recent call last):
  File "/home/pth/Workspace/finsy_test/finsy-master/examples/hello/./net/run.py", line 3, in <module>
    from finsy.test import demonet as dn
  File "/home/pth/.local/lib/python3.10/site-packages/finsy/test/demonet.py", line 458, in <module>
    assert _LOCAL_P4SWITCH_PY.exists()
AssertionError

I have installed the mininet and p4c environments in Ubuntu20.

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.